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 <catalog>} 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 <catalog>} 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 }