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 }