View Javadoc
1   /*
2    * $Id$
3    *
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *  http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  package org.apache.struts.extras.plugins;
22  
23  import java.io.File;
24  import java.io.FileNotFoundException;
25  import java.io.IOException;
26  import java.io.InputStream;
27  import java.net.MalformedURLException;
28  import java.net.URL;
29  import java.net.URLConnection;
30  
31  import jakarta.servlet.ServletException;
32  
33  import org.apache.commons.digester.Digester;
34  import org.apache.commons.digester.RuleSet;
35  import org.apache.commons.digester.xmlrules.DigesterLoader;
36  import org.apache.struts.action.ActionServlet;
37  import org.apache.struts.action.PlugIn;
38  import org.apache.struts.config.ModuleConfig;
39  import org.apache.struts.util.RequestUtils;
40  import org.slf4j.Logger;
41  import org.slf4j.LoggerFactory;
42  import org.xml.sax.SAXException;
43  
44  /**
45   * <p>An implementation of <code>PlugIn</code> which can be configured to
46   * instantiate a graph of objects using the Commons Digester and place the
47   * root object of that graph into the Application context.</p>
48   *
49   * @version $Rev$
50   * @see org.apache.struts.action.PlugIn
51   * @since Struts 1.2
52   */
53  public class DigestingPlugIn implements PlugIn {
54  
55      /**
56       * The {@code Log} instance for this class.
57       */
58      private final Logger log =
59          LoggerFactory.getLogger(DigestingPlugIn.class);
60  
61      protected static final String SOURCE_CLASSPATH = "classpath";
62      protected static final String SOURCE_FILE = "file";
63      protected static final String SOURCE_SERVLET = "servlet";
64      protected String configPath = null;
65      protected String configSource = SOURCE_SERVLET;
66      protected String digesterPath = null;
67      protected String digesterSource = SOURCE_SERVLET;
68      protected String key = null;
69      protected ModuleConfig moduleConfig = null;
70      protected String rulesets = null;
71      protected ActionServlet servlet = null;
72      protected boolean push = false;
73  
74      /**
75       * Constructor for DigestingPlugIn.
76       */
77      public DigestingPlugIn() {
78          super();
79      }
80  
81      /**
82       * Receive notification that our owning module is being shut down.
83       */
84      public void destroy() {
85          this.servlet = null;
86          this.moduleConfig = null;
87      }
88  
89      /**
90       * <p>Initialize a <code>Digester</code> and use it to parse a
91       * configuration file, resulting in a root object which will be placed
92       * into the ServletContext.</p>
93       *
94       * @param servlet ActionServlet that is managing all the modules in this
95       *                web application
96       * @param config  ModuleConfig for the module with which this plug-in is
97       *                associated
98       * @throws ServletException if this <code>PlugIn</code> cannot be
99       *                          successfully initialized
100      */
101     public void init(ActionServlet servlet, ModuleConfig config)
102         throws ServletException {
103         this.servlet = servlet;
104         this.moduleConfig = config;
105 
106         Object obj = null;
107 
108         Digester digester = this.initializeDigester();
109 
110         if (this.push) {
111             log.debug("push == true; pushing plugin onto digester stack");
112             digester.push(this);
113         }
114 
115         try {
116             log.debug("XML data file: [path: {}, source: {}]",
117                 this.configPath, this.configSource);
118 
119             URL configURL =
120                 this.getConfigURL(this.configPath, this.configSource);
121 
122             if (configURL == null) {
123                 throw new ServletException(
124                     "Unable to locate XML data file at [path: "
125                     + this.configPath + ", source: " + this.configSource + "]");
126             }
127 
128             URLConnection conn = configURL.openConnection();
129 
130             conn.setUseCaches(false);
131             conn.connect();
132             try (InputStream is = conn.getInputStream()) {
133                 obj = digester.parse(is);
134             }
135         } catch (IOException e) {
136             // TODO Internationalize msg
137             log.error("Exception processing config", e);
138             throw new ServletException(e);
139         } catch (SAXException e) {
140             // TODO Internationalize msg
141             log.error("Exception processing config", e);
142             throw new ServletException(e);
143         }
144 
145         this.storeGeneratedObject(obj);
146     }
147 
148     /**
149      * Initialize the <code>Digester</code> which will be used to process the
150      * main configuration.
151      *
152      * @return a Digester, ready to use.
153      * @throws ServletException
154      */
155     protected Digester initializeDigester()
156         throws ServletException {
157         Digester digester = null;
158 
159         if ((this.digesterPath != null) && (this.digesterSource != null)) {
160             try {
161                 log.debug("Initialize digester from XML [path: {}; source: {}]",
162                     this.digesterPath, this.digesterSource);
163                 digester =
164                     this.digesterFromXml(this.digesterPath, this.digesterSource);
165             } catch (IOException e) {
166                 // TODO Internationalize msg
167                 log.error("Exception instantiating digester from XML", e);
168                 throw new ServletException(e);
169             }
170         } else {
171             log.debug("No XML rules for digester; call newDigesterInstance()");
172             digester = this.newDigesterInstance();
173         }
174 
175         try {
176             digester.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
177         } catch (Exception e) {
178             log.error("Exception configuring Digester instance", e);
179             throw new ServletException(e);
180         }
181 
182         this.applyRuleSets(digester);
183 
184         return digester;
185     }
186 
187     /**
188      * <p>Instantiate a <code>Digester</code>.</p> <p>Subclasses may wish to
189      * override this to provide a subclass of Digester, or to configure the
190      * Digester using object methods.</p>
191      *
192      * @return a basic instance of <code>org.apache.commons.digester.Digester</code>
193      */
194     protected Digester newDigesterInstance() {
195         return new Digester();
196     }
197 
198     /**
199      * <p>Instantiate a Digester from an XML input stream using the Commons
200      * <code>DigesterLoader</code>.</p>
201      *
202      * @param path   the path to the digester rules XML to be found using
203      *               <code>source</code>
204      * @param source a string indicating the lookup method to be used with
205      *               <code>path</code>
206      * @return a configured Digester
207      * @throws FileNotFoundException
208      * @throws MalformedURLException
209      * @see #getConfigURL(String, String)
210      */
211     protected Digester digesterFromXml(String path, String source)
212         throws IOException {
213         URL configURL = this.getConfigURL(path, source);
214 
215         if (configURL == null) {
216             throw new NullPointerException("No resource '" + path
217                 + "' found in '" + source + "'");
218         }
219 
220 //        Digester3:
221 //        RulesModule rules = new FromXmlRulesModule() {
222 //            @Override
223 //            protected void loadRules() {
224 //                loadXMLRules(configURL);
225 //            }
226 //        };
227 //        return DigesterLoader.newLoader(rules).newDigester();
228         return DigesterLoader.createDigester(configURL);
229     }
230 
231     /**
232      * Instantiate any <code>RuleSet</code> classes defined in the
233      * <code>rulesets</code> property and use them to add rules to our
234      * <code>Digester</code>.
235      *
236      * @param digester the Digester instance to add RuleSet objects to.
237      * @throws ServletException
238      */
239     protected void applyRuleSets(Digester digester)
240         throws ServletException {
241         if ((this.rulesets == null) || (this.rulesets.trim().length() == 0)) {
242             return;
243         }
244 
245         rulesets = rulesets.trim();
246 
247         String ruleSet = null;
248 
249         while (rulesets.length() > 0) {
250             int comma = rulesets.indexOf(",");
251 
252             if (comma < 0) {
253                 ruleSet = rulesets.trim();
254                 rulesets = "";
255             } else {
256                 ruleSet = rulesets.substring(0, comma).trim();
257                 rulesets = rulesets.substring(comma + 1).trim();
258             }
259 
260             // TODO Internationalize msg
261             log.debug("Configuring custom Digester Ruleset of type {}"
262                 , ruleSet);
263 
264             try {
265                 RuleSet instance =
266                     (RuleSet) RequestUtils.applicationInstance(ruleSet);
267 
268                 digester.addRuleSet(instance);
269             } catch (Exception e) {
270                 // TODO Internationalize msg
271                 log.error("Exception configuring custom Digester RuleSet", e);
272                 throw new ServletException(e);
273             }
274         }
275     }
276 
277     /**
278      * <p>Look up a resource path using one of a set of known path resolution
279      * mechanisms and return a URL to the resource.</p>
280      *
281      * @param path   a String which is meaningful to one of the known
282      *               resolution mechanisms.
283      * @param source one of the known path resolution mechanisms:
284      *
285      *               <ul>
286      *
287      *               <li>file - the path is a fully-qualified filesystem
288      *               path.</li>
289      *
290      *               <li>servlet - the path is a servlet-context relative
291      *               path.</li>
292      *
293      *               <li>classpath - the path is a classpath-relative
294      *               path.</li>
295      *
296      *               </ul>
297      * @return a URL pointing to the given path in the given mechanism.
298      * @throws java.io.FileNotFoundException
299      * @throws java.net.MalformedURLException
300      */
301     protected URL getConfigURL(String path, String source)
302         throws IOException {
303         if (SOURCE_CLASSPATH.equals(source)) {
304             return this.getClassPathURL(path);
305         }
306 
307         if (SOURCE_FILE.equals(source)) {
308             return this.getFileURL(path);
309         }
310 
311         if (SOURCE_SERVLET.equals(source)) {
312             return this.getServletContextURL(path);
313         }
314 
315         // TODO Internationalize msg
316         throw new IllegalArgumentException("ConfigSource " + source
317             + " is not recognized");
318     }
319 
320     /**
321      * Given a string, return a URL to a classpath resource of that name.
322      *
323      * @param path a Classpath-relative string identifying a resource.
324      * @return a URL identifying the resource on the classpath. TODO Do we
325      *         need to be smarter about ClassLoaders?
326      */
327     protected URL getClassPathURL(String path) {
328         return getClass().getClassLoader().getResource(path);
329     }
330 
331     /**
332      * Given a string, return a URL to a Servlet Context resource of that
333      * name.
334      *
335      * @param path a Classpath-relative string identifying a resource.
336      * @return a URL identifying the resource in the Servlet Context
337      * @throws MalformedURLException
338      */
339     protected URL getServletContextURL(String path)
340         throws IOException {
341         return this.servlet.getServletContext().getResource(path);
342     }
343 
344     /**
345      * Given a string, return a URL to a Filesystem resource of that name.
346      *
347      * @param path a path to a file.
348      * @return a URL identifying the resource in the in the file system.
349      * @throws MalformedURLException
350      * @throws FileNotFoundException
351      */
352     protected URL getFileURL(String path)
353         throws IOException {
354         File file = new File(path);
355 
356         return file.toURI().toURL();
357     }
358 
359     /**
360      * @param configPath the path to configuration information for this
361      *                   PlugIn.
362      * @see #configSource
363      */
364     public void setConfigPath(String configPath) {
365         this.configPath = configPath;
366     }
367 
368     /**
369      * @return the configPath property
370      * @see #configSource
371      */
372     public String getConfigPath() {
373         return configPath;
374     }
375 
376     /**
377      * Set the source of the config file.  Should be one of the following:
378      * <ul> <li> "classpath" - indicates that the configPath will be resolved
379      * by the ClassLoader. </li> <li> "file" - indicates that the configPath
380      * is a fully-qualified filesystem path. </li> <li> "servlet" - indicates
381      * that the configPath will be found by the ServletContext. </li> </ul>
382      *
383      * @param configSource the source (lookup method) for the config file.
384      * @see #configPath
385      */
386     public void setConfigSource(String configSource) {
387         this.configSource = configSource;
388     }
389 
390     /**
391      * @return the string describing which access method should be used to
392      *         resolve configPath.
393      * @see #configPath
394      */
395     public String getConfigSource() {
396         return configSource;
397     }
398 
399     /**
400      * This method is called after the Digester runs to store the generated
401      * object somewhere.  This implementation places the given object into the
402      * ServletContext under the attribute name as defined in
403      * <code>key</code>.
404      *
405      * @param obj The object to save.
406      */
407     protected void storeGeneratedObject(Object obj) {
408         log.debug("Put [{}] into application context [key:{}]",
409             obj, this.key);
410         this.servlet.getServletContext().setAttribute(this.getKey(), obj);
411     }
412 
413     /**
414      * @param key The ServletContext attribute name to store the generated
415      *            object under.
416      */
417     public void setKey(String key) {
418         this.key = key;
419     }
420 
421     /**
422      * @return The ServletContext attribute name the generated object is
423      *         stored under.
424      */
425     public String getKey() {
426         return key;
427     }
428 
429     /**
430      * <p>A comma-delimited list of one or more classes which implement
431      * <code>org.apache.commons.digester.RuleSet</code>. (Optional)</p>
432      */
433     public void setRulesets(String ruleSets) {
434         this.rulesets = ruleSets;
435     }
436 
437     /**
438      * @return The configured list of <code>RuleSet</code> classes.
439      */
440     public String getRulesets() {
441         return this.rulesets;
442     }
443 
444     /**
445      * <p>The path to a Digester XML configuration file, relative to the
446      * <code>digesterSource</code> property. (Optional)</p>
447      *
448      * @see #digesterSource
449      * @see #getConfigURL(String, String)
450      */
451     public void setDigesterPath(String digesterPath) {
452         this.digesterPath = digesterPath;
453     }
454 
455     /**
456      * @return the configured path to a Digester XML config file, or null.
457      * @see #digesterSource
458      * @see #getConfigURL(String, String)
459      */
460     public String getDigesterPath() {
461         return digesterPath;
462     }
463 
464     /**
465      * <p>The lookup mechanism to be used to resolve <code>digesterPath</code>
466      * (optional). </p>
467      *
468      * @param digesterSource
469      * @see #getConfigURL(String, String)
470      */
471     public void setDigesterSource(String digesterSource) {
472         this.digesterSource = digesterSource;
473     }
474 
475     /**
476      * @return the configured lookup mechanism for resolving
477      *         <code>digesterPath</code>.
478      * @see #getConfigURL(String, String)
479      */
480     public String getDigesterSource() {
481         return this.digesterSource;
482     }
483 
484     /**
485      * <p>If set to <code>true</code>, this PlugIn will be pushed onto the
486      * Digester stack before the digester <code>parse</code> method is
487      * called.</p> <p>Defaults to <code>false</code></p>
488      *
489      * @param push
490      */
491     public void setPush(boolean push) {
492         this.push = push;
493     }
494 
495     /**
496      * @return Whether or not this <code>PlugIn</code> instance will be pushed
497      *         onto the <code>Digester</code> stack before
498      *         <code>digester.parse()</code> is called.
499      */
500     public boolean getPush() {
501         return this.push;
502     }
503 }