View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *     http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.chain.web.jakarta;
18  
19  import java.io.InputStream;
20  import java.net.URL;
21  import java.util.Collections;
22  import java.util.Iterator;
23  import java.util.Set;
24  
25  import org.apache.commons.chain.Catalog;
26  import org.apache.commons.chain.CatalogFactory;
27  import org.apache.commons.chain.config.ConfigParser;
28  import org.apache.commons.chain.impl.CatalogBase;
29  import org.apache.commons.chain.web.CheckedConsumer;
30  import org.apache.commons.digester.RuleSet;
31  import org.slf4j.Logger;
32  
33  import jakarta.servlet.ServletContext;
34  import jakarta.servlet.ServletException;
35  
36  /**
37   * Context-initializer that automatically scans chain configuration files
38   * in the current web application at startup time, and exposes the result
39   * in a {@link Catalog} under a specified servlet context attribute. The
40   * following <em>context</em> init parameters are utilized:
41   * <ul>
42   * <li><strong>org.apache.commons.chain.CONFIG_CLASS_RESOURCE</strong> -
43   *     comma-delimited list of chain configuration resources to be loaded
44   *     via {@code ClassLoader.getResource()} calls. If not specified,
45   *     no class loader resources will be loaded.</li>
46   * <li><strong>org.apache.commons.chain.CONFIG_WEB_RESOURCE</strong> -
47   *     comma-delimited list of chain configuration webapp resources
48   *     to be loaded. If not specified, no web application resources
49   *     will be loaded.</li>
50   * <li><strong>org.apache.commons.chain.CONFIG_ATTR</strong> -
51   *     Name of the servlet context attribute under which the
52   *     resulting {@link Catalog} will be created or updated.
53   *     If not specified, it is expected that parsed resources will
54   *     contain {@code &lt;catalog&gt;} elements (which will
55   *     cause registration of the created {@link Catalog}s into
56   *     the {@link CatalogFactory} for this application, and no
57   *     servet context attribute will be created.
58   *     <strong>NOTE</strong> - This parameter is deprecated.</li>
59   * <li><strong>org.apache.commons.chain.RULE_SET</strong> -
60   *     Fully qualified class name of a Digester {@code RuleSet}
61   *     implementation to use for parsing configuration resources (this
62   *     class must have a public zero-args constructor). If not defined,
63   *     the standard {@code RuleSet} implementation will be used.</li>
64   * </ul>
65   *
66   * <p>When a web application that has configured this listener is
67   * started, it will acquire the {@link Catalog} under the specified servlet
68   * context attribute key, creating a new one if there is none already there.
69   * This {@link Catalog} will then be populated by scanning configuration
70   * resources from the following sources (loaded in this order):</p>
71   * <ul>
72   * <li>Optional: Resources loaded from any {@code META-INF/chain-config.xml}
73   *     resource found in a JAR file in {@code /WEB-INF/lib}.</li>
74   * <li>Resources loaded from specified resource paths from the
75   *     webapp's class loader (via {@code ClassLoader.getResource()}).</li>
76   * <li>Resources loaded from specified resource paths in the web application
77   *     archive (via {@code ServetContext.getResource()}).</li>
78   * </ul>
79   *
80   * <p>If no attribute key is specified, on the other hand, parsed configuration
81   * resources are expected to contain {@code &lt;catalog&gt;} elements,
82   * and the catalogs will be registered with the {@link CatalogFactory}
83   * for this web application.</p>
84   *
85   * @author Craig R. McClanahan
86   * @author Ted Husted
87   */
88  final class ChainInit {
89  
90      /**
91       * The name of the context init parameter containing the name of the
92       * servlet context attribute under which our resulting {@link Catalog}
93       * will be stored.
94       */
95      static final String CONFIG_ATTR =
96          "org.apache.commons.chain.CONFIG_ATTR";
97  
98      /**
99       * The name of the context init parameter containing a comma-delimited
100      * list of class loader resources to be scanned.
101      */
102     static final String CONFIG_CLASS_RESOURCE =
103         "org.apache.commons.chain.CONFIG_CLASS_RESOURCE";
104 
105     /**
106      * The name of the context init parameter containing a comma-delimited
107      * list of web application resources to be scanned.
108      */
109     static final String CONFIG_WEB_RESOURCE =
110         "org.apache.commons.chain.CONFIG_WEB_RESOURCE";
111 
112     /**
113      * The name of the context init parameter containing the fully
114      * qualified class name of the {@code RuleSet} implementation
115      * for configuring our {@link ConfigParser}.
116      */
117     static final String RULE_SET =
118         "org.apache.commons.chain.RULE_SET";
119 
120     /**
121      * Remove the configured {@link Catalog} from the servlet context
122      * attributes for this web application.
123      *
124      * @param context the servlet-context
125      * @param attr the value of the {@code CONFIG_ATTR}
126      */
127     static void destroy(ServletContext context, String attr) {
128         if (attr != null) {
129             context.removeAttribute(attr);
130         }
131         CatalogFactory.clear();
132     }
133 
134     /**
135      * Private constructor.
136      */
137     private ChainInit() {
138     }
139 
140     /**
141      * Scan the required chain configuration resources, assemble the
142      * configured chains into a {@link Catalog}, and expose it as a
143      * servlet context attribute under the specified key.
144      *
145      * @param context the servlet-context
146      * @param attr the value of the {@code CONFIG_ATTR}
147      * @param logger to use for logging
148      * @param parseJarResources {@code true} to parse resources in jar-files
149      */
150     @SuppressWarnings("deprecation")
151     static void initialize(ServletContext context, String attr, Logger logger, boolean parseJarResources) throws ServletException {
152         String classResources = context.getInitParameter(CONFIG_CLASS_RESOURCE);
153         String ruleSet = context.getInitParameter(RULE_SET);
154         String webResources = context.getInitParameter(CONFIG_WEB_RESOURCE);
155 
156         // Retrieve or create the Catalog instance we may be updating
157         Catalog<?> catalog = null;
158         if (attr != null) {
159             catalog = (Catalog<?>) context.getAttribute(attr);
160             if (catalog == null) {
161                 catalog = new CatalogBase<>();
162             }
163         }
164 
165         // Construct the configuration resource parser we will use
166         ConfigParser parser = new ConfigParser();
167         if (ruleSet != null) {
168             try {
169                 ClassLoader loader =
170                     Thread.currentThread().getContextClassLoader();
171                 if (loader == null) {
172                     loader = ChainInit.class.getClassLoader();
173                 }
174                 Class<? extends RuleSet> clazz = loader
175                         .loadClass(ruleSet)
176                         .asSubclass(RuleSet.class);
177                 parser.setRuleSet(clazz.getDeclaredConstructor().newInstance());
178             } catch (Exception e) {
179                 throw new ServletException("Exception initalizing RuleSet '"
180                                            + ruleSet + "' instance ", e);
181             }
182         }
183 
184         // Parse the resources specified in our init parameters (if any)
185         final CheckedConsumer<URL, Exception> parse;
186         if (attr == null) {
187             parse = parser::parse;
188         } else {
189             final Catalog<?> cat = catalog;
190             parse = url -> parser.parse(cat, url);
191         }
192         if (parseJarResources) {
193             parseJarResources(context, parse, logger);
194         }
195         ChainResources.parseClassResources(classResources, parse);
196         ChainResources.parseWebResources(context, webResources, parse);
197 
198         // Expose the completed catalog (if requested)
199         if (attr != null) {
200             context.setAttribute(attr, catalog);
201         }
202     }
203 
204     // --------------------------------------------------------- Private Methods
205 
206     /**
207      * Parse resources found in JAR files in the {@code /WEB-INF/lib}
208      * subdirectory (if any).
209      *
210      * @param <E> the type of the exception from parse-function
211      * @param context {@code ServletContext} for this web application
212      * @param parse parse-function to parse the XML document
213      * @param logger to use for logging
214      */
215     private static <E extends Exception> void parseJarResources(ServletContext context,
216                 CheckedConsumer<URL, E> parse, Logger logger) {
217 
218         Set<String> jars = context.getResourcePaths("/WEB-INF/lib");
219         if (jars == null) {
220             jars = Collections.emptySet();
221         }
222         String path = null;
223         Iterator<String> paths = jars.iterator();
224         while (paths.hasNext()) {
225 
226             path = paths.next();
227             if (!path.endsWith(".jar")) {
228                 continue;
229             }
230             URL resourceURL = null;
231             try {
232                 URL jarURL = context.getResource(path);
233                 path = jarURL.toExternalForm();
234 
235                 resourceURL = new URL("jar:"
236                                       + translate(path)
237                                       + "!/META-INF/chain-config.xml");
238                 path = resourceURL.toExternalForm();
239 
240                 InputStream is = null;
241                 try {
242                     is = resourceURL.openStream();
243                 } catch (Exception e) {
244                     // means there is no such resource
245                     logger.atTrace().setMessage("OpenStream: {}").addArgument(resourceURL).setCause(e).log();
246                 }
247                 if (is == null) {
248                     logger.debug("Not Found: {}", resourceURL);
249                     continue;
250                 } else {
251                     is.close();
252                 }
253                 logger.debug("Parsing: {}", resourceURL);
254                 parse.accept(resourceURL);
255             } catch (Exception e) {
256                 throw new RuntimeException("Exception parsing chain config resource '"
257                      + path + "': " + e.getMessage());
258             }
259         }
260     }
261 
262     /**
263      * Translate space character into {@code %20} to avoid problems
264      * with paths that contain spaces on some JVMs.
265      *
266      * @param value Value to translate
267      *
268      * @return the translated value
269      */
270     private static String translate(String value) {
271         while (true) {
272             int index = value.indexOf(' ');
273             if (index < 0) {
274                 break;
275             }
276             value = value.substring(0, index) + "%20" + value.substring(index + 1);
277         }
278         return value;
279     }
280 }