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
18
19 package org.apache.catalina.startup;
20
21
22 import java.io.File;
23 import java.io.IOException;
24 import java.io.InputStream;
25 import java.net.URISyntaxException;
26 import java.net.URL;
27 import java.net.URLClassLoader;
28 import java.util.ArrayList;
29 import java.util.Enumeration;
30 import java.util.HashMap;
31 import java.util.HashSet;
32 import java.util.Iterator;
33 import java.util.Map;
34 import java.util.Set;
35 import java.util.StringTokenizer;
36 import java.util.jar.JarEntry;
37 import java.util.jar.JarFile;
38
39 import javax.naming.NameClassPair;
40 import javax.naming.NamingEnumeration;
41 import javax.naming.NamingException;
42 import javax.naming.directory.DirContext;
43 import javax.servlet.ServletException;
44
45 import org.apache.catalina.Context;
46 import org.apache.catalina.Lifecycle;
47 import org.apache.catalina.LifecycleEvent;
48 import org.apache.catalina.LifecycleListener;
49 import org.apache.catalina.core.StandardContext;
50 import org.apache.catalina.core.StandardHost;
51 import org.apache.catalina.util.StringManager;
52 import org.apache.tomcat.util.digester.Digester;
53 import org.xml.sax.InputSource;
54
55 /**
56 * Startup event listener for a <b>Context</b> that configures application
57 * listeners configured in any TLD files.
58 *
59 * @author Craig R. McClanahan
60 * @author Jean-Francois Arcand
61 * @author Costin Manolache
62 */
63 public final class TldConfig implements LifecycleListener {
64
65 // Names of JARs that are known not to contain any TLDs
66 private static HashSet<String> noTldJars;
67
68 private static org.apache.juli.logging.Log log=
69 org.apache.juli.logging.LogFactory.getLog( TldConfig.class );
70
71 /*
72 * Initializes the set of JARs that are known not to contain any TLDs
73 */
74 static {
75 noTldJars = new HashSet<String>();
76 // Bootstrap JARs
77 noTldJars.add("bootstrap.jar");
78 noTldJars.add("commons-daemon.jar");
79 noTldJars.add("tomcat-juli.jar");
80 // Main JARs
81 noTldJars.add("annotations-api.jar");
82 noTldJars.add("catalina.jar");
83 noTldJars.add("catalina-ant.jar");
84 noTldJars.add("catalina-ha.jar");
85 noTldJars.add("catalina-tribes.jar");
86 noTldJars.add("el-api.jar");
87 noTldJars.add("jasper.jar");
88 noTldJars.add("jasper-el.jar");
89 noTldJars.add("jasper-jdt.jar");
90 noTldJars.add("jsp-api.jar");
91 noTldJars.add("servlet-api.jar");
92 noTldJars.add("tomcat-coyote.jar");
93 noTldJars.add("tomcat-dbcp.jar");
94 // i18n JARs
95 noTldJars.add("tomcat-i18n-en.jar");
96 noTldJars.add("tomcat-i18n-es.jar");
97 noTldJars.add("tomcat-i18n-fr.jar");
98 noTldJars.add("tomcat-i18n-ja.jar");
99 // Misc JARs not included with Tomcat
100 noTldJars.add("ant.jar");
101 noTldJars.add("commons-dbcp.jar");
102 noTldJars.add("commons-beanutils.jar");
103 noTldJars.add("commons-fileupload-1.0.jar");
104 noTldJars.add("commons-pool.jar");
105 noTldJars.add("commons-digester.jar");
106 noTldJars.add("commons-logging.jar");
107 noTldJars.add("commons-collections.jar");
108 noTldJars.add("jmx.jar");
109 noTldJars.add("jmx-tools.jar");
110 noTldJars.add("xercesImpl.jar");
111 noTldJars.add("xmlParserAPIs.jar");
112 noTldJars.add("xml-apis.jar");
113 // JARs from J2SE runtime
114 noTldJars.add("sunjce_provider.jar");
115 noTldJars.add("ldapsec.jar");
116 noTldJars.add("localedata.jar");
117 noTldJars.add("dnsns.jar");
118 noTldJars.add("tools.jar");
119 noTldJars.add("sunpkcs11.jar");
120 }
121
122
123 // ----------------------------------------------------- Instance Variables
124
125 /**
126 * The Context we are associated with.
127 */
128 private Context context = null;
129
130
131 /**
132 * The string resources for this package.
133 */
134 private static final StringManager sm =
135 StringManager.getManager(Constants.Package);
136
137 /**
138 * The <code>Digester</code> we will use to process tag library
139 * descriptor files.
140 */
141 private static Digester tldDigester = null;
142
143
144 /**
145 * Attribute value used to turn on/off TLD validation
146 */
147 private static boolean tldValidation = false;
148
149
150 /**
151 * Attribute value used to turn on/off TLD namespace awarenes.
152 */
153 private static boolean tldNamespaceAware = false;
154
155 private boolean rescan=true;
156
157 private ArrayList<String> listeners = new ArrayList<String>();
158
159 // --------------------------------------------------------- Public Methods
160
161 /**
162 * Sets the list of JARs that are known not to contain any TLDs.
163 *
164 * @param jarNames List of comma-separated names of JAR files that are
165 * known not to contain any TLDs
166 */
167 public static void setNoTldJars(String jarNames) {
168 if (jarNames != null) {
169 noTldJars.clear();
170 StringTokenizer tokenizer = new StringTokenizer(jarNames, ",");
171 while (tokenizer.hasMoreElements()) {
172 noTldJars.add(tokenizer.nextToken());
173 }
174 }
175 }
176
177 /**
178 * Set the validation feature of the XML parser used when
179 * parsing xml instances.
180 * @param tldValidation true to enable xml instance validation
181 */
182 public void setTldValidation(boolean tldValidation){
183 TldConfig.tldValidation = tldValidation;
184 }
185
186 /**
187 * Get the server.xml <host> attribute's xmlValidation.
188 * @return true if validation is enabled.
189 *
190 */
191 public boolean getTldValidation(){
192 return tldValidation;
193 }
194
195 /**
196 * Get the server.xml <host> attribute's xmlNamespaceAware.
197 * @return true if namespace awarenes is enabled.
198 *
199 */
200 public boolean getTldNamespaceAware(){
201 return tldNamespaceAware;
202 }
203
204
205 /**
206 * Set the namespace aware feature of the XML parser used when
207 * parsing xml instances.
208 * @param tldNamespaceAware true to enable namespace awareness
209 */
210 public void setTldNamespaceAware(boolean tldNamespaceAware){
211 TldConfig.tldNamespaceAware = tldNamespaceAware;
212 }
213
214
215 public boolean isRescan() {
216 return rescan;
217 }
218
219 public void setRescan(boolean rescan) {
220 this.rescan = rescan;
221 }
222
223 public Context getContext() {
224 return context;
225 }
226
227 public void setContext(Context context) {
228 this.context = context;
229 }
230
231 public void addApplicationListener( String s ) {
232 //if(log.isDebugEnabled())
233 log.debug( "Add tld listener " + s);
234 listeners.add(s);
235 }
236
237 public String[] getTldListeners() {
238 String result[]=new String[listeners.size()];
239 listeners.toArray(result);
240 return result;
241 }
242
243
244 /**
245 * Scan for and configure all tag library descriptors found in this
246 * web application.
247 *
248 * @exception Exception if a fatal input/output or parsing error occurs
249 */
250 public void execute() throws Exception {
251 long t1=System.currentTimeMillis();
252
253 /*
254 * Acquire the list of TLD resource paths, possibly embedded in JAR
255 * files, to be processed
256 */
257 Set resourcePaths = tldScanResourcePaths();
258 Map jarPaths = getJarPaths();
259
260 // Scan each accumulated resource path for TLDs to be processed
261 Iterator paths = resourcePaths.iterator();
262 while (paths.hasNext()) {
263 String path = (String) paths.next();
264 if (path.endsWith(".jar")) {
265 tldScanJar(path);
266 } else {
267 tldScanTld(path);
268 }
269 }
270 if (jarPaths != null) {
271 paths = jarPaths.values().iterator();
272 while (paths.hasNext()) {
273 tldScanJar((File) paths.next());
274 }
275 }
276
277 String list[] = getTldListeners();
278
279 if( log.isDebugEnabled() )
280 log.debug( "Adding tld listeners:" + list.length);
281 for( int i=0; list!=null && i<list.length; i++ ) {
282 context.addApplicationListener(list[i]);
283 }
284
285 long t2=System.currentTimeMillis();
286 if( context instanceof StandardContext ) {
287 ((StandardContext)context).setTldScanTime(t2-t1);
288 }
289
290 }
291
292 // -------------------------------------------------------- Private Methods
293
294 /**
295 * Scan the JAR file at the specified resource path for TLDs in the
296 * <code>META-INF</code> subdirectory, and scan each TLD for application
297 * event listeners that need to be registered.
298 *
299 * @param resourcePath Resource path of the JAR file to scan
300 *
301 * @exception Exception if an exception occurs while scanning this JAR
302 */
303 private void tldScanJar(String resourcePath) throws Exception {
304
305 if (log.isDebugEnabled()) {
306 log.debug(" Scanning JAR at resource path '" + resourcePath + "'");
307 }
308
309 URL url = context.getServletContext().getResource(resourcePath);
310 if (url == null) {
311 throw new IllegalArgumentException
312 (sm.getString("contextConfig.tldResourcePath",
313 resourcePath));
314 }
315
316 File file = null;
317 try {
318 file = new File(url.toURI());
319 } catch (URISyntaxException e) {
320 // Ignore, probably an unencoded char
321 file = new File(url.getFile());
322 }
323 try {
324 file = file.getCanonicalFile();
325 } catch (IOException e) {
326 // Ignore
327 }
328 tldScanJar(file);
329
330 }
331
332 /**
333 * Scans all TLD entries in the given JAR for application listeners.
334 *
335 * @param file JAR file whose TLD entries are scanned for application
336 * listeners
337 */
338 private void tldScanJar(File file) throws Exception {
339
340 JarFile jarFile = null;
341 String name = null;
342
343 String jarPath = file.getAbsolutePath();
344
345 try {
346 jarFile = new JarFile(file);
347 Enumeration entries = jarFile.entries();
348 while (entries.hasMoreElements()) {
349 JarEntry entry = (JarEntry) entries.nextElement();
350 name = entry.getName();
351 if (!name.startsWith("META-INF/")) {
352 continue;
353 }
354 if (!name.endsWith(".tld")) {
355 continue;
356 }
357 if (log.isTraceEnabled()) {
358 log.trace(" Processing TLD at '" + name + "'");
359 }
360 try {
361 tldScanStream(new InputSource(jarFile.getInputStream(entry)));
362 } catch (Exception e) {
363 log.error(sm.getString("contextConfig.tldEntryException",
364 name, jarPath, context.getPath()),
365 e);
366 }
367 }
368 } catch (Exception e) {
369 log.error(sm.getString("contextConfig.tldJarException",
370 jarPath, context.getPath()),
371 e);
372 } finally {
373 if (jarFile != null) {
374 try {
375 jarFile.close();
376 } catch (Throwable t) {
377 // Ignore
378 }
379 }
380 }
381 }
382
383 /**
384 * Scan the TLD contents in the specified input stream, and register
385 * any application event listeners found there. <b>NOTE</b> - It is
386 * the responsibility of the caller to close the InputStream after this
387 * method returns.
388 *
389 * @param resourceStream InputStream containing a tag library descriptor
390 *
391 * @exception Exception if an exception occurs while scanning this TLD
392 */
393 private void tldScanStream(InputSource resourceStream)
394 throws Exception {
395
396 synchronized (tldDigester) {
397 try {
398 tldDigester.push(this);
399 tldDigester.parse(resourceStream);
400 } finally {
401 tldDigester.reset();
402 }
403 }
404
405 }
406
407 /**
408 * Scan the TLD contents at the specified resource path, and register
409 * any application event listeners found there.
410 *
411 * @param resourcePath Resource path being scanned
412 *
413 * @exception Exception if an exception occurs while scanning this TLD
414 */
415 private void tldScanTld(String resourcePath) throws Exception {
416
417 if (log.isDebugEnabled()) {
418 log.debug(" Scanning TLD at resource path '" + resourcePath + "'");
419 }
420
421 InputSource inputSource = null;
422 try {
423 InputStream stream =
424 context.getServletContext().getResourceAsStream(resourcePath);
425 if (stream == null) {
426 throw new IllegalArgumentException
427 (sm.getString("contextConfig.tldResourcePath",
428 resourcePath));
429 }
430 inputSource = new InputSource(stream);
431 if (inputSource == null) {
432 throw new IllegalArgumentException
433 (sm.getString("contextConfig.tldResourcePath",
434 resourcePath));
435 }
436 tldScanStream(inputSource);
437 } catch (Exception e) {
438 throw new ServletException
439 (sm.getString("contextConfig.tldFileException", resourcePath,
440 context.getPath()),
441 e);
442 }
443
444 }
445
446 /**
447 * Accumulate and return a Set of resource paths to be analyzed for
448 * tag library descriptors. Each element of the returned set will be
449 * the context-relative path to either a tag library descriptor file,
450 * or to a JAR file that may contain tag library descriptors in its
451 * <code>META-INF</code> subdirectory.
452 *
453 * @exception IOException if an input/output error occurs while
454 * accumulating the list of resource paths
455 */
456 private Set tldScanResourcePaths() throws IOException {
457 if (log.isDebugEnabled()) {
458 log.debug(" Accumulating TLD resource paths");
459 }
460 Set resourcePaths = new HashSet();
461
462 // Accumulate resource paths explicitly listed in the web application
463 // deployment descriptor
464 if (log.isTraceEnabled()) {
465 log.trace(" Scanning <taglib> elements in web.xml");
466 }
467 String taglibs[] = context.findTaglibs();
468 for (int i = 0; i < taglibs.length; i++) {
469 String resourcePath = context.findTaglib(taglibs[i]);
470 // FIXME - Servlet 2.4 DTD implies that the location MUST be
471 // a context-relative path starting with '/'?
472 if (!resourcePath.startsWith("/")) {
473 resourcePath = "/WEB-INF/" + resourcePath;
474 }
475 if (log.isTraceEnabled()) {
476 log.trace(" Adding path '" + resourcePath +
477 "' for URI '" + taglibs[i] + "'");
478 }
479 resourcePaths.add(resourcePath);
480 }
481
482 DirContext resources = context.getResources();
483 if (resources != null) {
484 tldScanResourcePathsWebInf(resources, "/WEB-INF", resourcePaths);
485 }
486
487 // Return the completed set
488 return (resourcePaths);
489
490 }
491
492 /*
493 * Scans the web application's subdirectory identified by rootPath,
494 * along with its subdirectories, for TLDs.
495 *
496 * Initially, rootPath equals /WEB-INF. The /WEB-INF/classes and
497 * /WEB-INF/lib subdirectories are excluded from the search, as per the
498 * JSP 2.0 spec.
499 *
500 * @param resources The web application's resources
501 * @param rootPath The path whose subdirectories are to be searched for
502 * TLDs
503 * @param tldPaths The set of TLD resource paths to add to
504 */
505 private void tldScanResourcePathsWebInf(DirContext resources,
506 String rootPath,
507 Set tldPaths)
508 throws IOException {
509
510 if (log.isTraceEnabled()) {
511 log.trace(" Scanning TLDs in " + rootPath + " subdirectory");
512 }
513
514 try {
515 NamingEnumeration items = resources.list(rootPath);
516 while (items.hasMoreElements()) {
517 NameClassPair item = (NameClassPair) items.nextElement();
518 String resourcePath = rootPath + "/" + item.getName();
519 if (!resourcePath.endsWith(".tld")
520 && (resourcePath.startsWith("/WEB-INF/classes")
521 || resourcePath.startsWith("/WEB-INF/lib"))) {
522 continue;
523 }
524 if (resourcePath.endsWith(".tld")) {
525 if (log.isTraceEnabled()) {
526 log.trace(" Adding path '" + resourcePath + "'");
527 }
528 tldPaths.add(resourcePath);
529 } else {
530 tldScanResourcePathsWebInf(resources, resourcePath,
531 tldPaths);
532 }
533 }
534 } catch (NamingException e) {
535 ; // Silent catch: it's valid that no /WEB-INF directory exists
536 }
537 }
538
539 /**
540 * Returns a map of the paths to all JAR files that are accessible to the
541 * webapp and will be scanned for TLDs.
542 *
543 * The map always includes all the JARs under WEB-INF/lib, as well as
544 * shared JARs in the classloader delegation chain of the webapp's
545 * classloader.
546 *
547 * The latter constitutes a Tomcat-specific extension to the TLD search
548 * order defined in the JSP spec. It allows tag libraries packaged as JAR
549 * files to be shared by web applications by simply dropping them in a
550 * location that all web applications have access to (e.g.,
551 * <CATALINA_HOME>/common/lib).
552 *
553 * The set of shared JARs to be scanned for TLDs is narrowed down by
554 * the <tt>noTldJars</tt> class variable, which contains the names of JARs
555 * that are known not to contain any TLDs.
556 *
557 * @return Map of JAR file paths
558 */
559 private Map getJarPaths() {
560
561 HashMap jarPathMap = null;
562
563 ClassLoader webappLoader = Thread.currentThread().getContextClassLoader();
564 ClassLoader loader = webappLoader;
565 while (loader != null) {
566 if (loader instanceof URLClassLoader) {
567 URL[] urls = ((URLClassLoader) loader).getURLs();
568 for (int i=0; i<urls.length; i++) {
569 // Expect file URLs, these are %xx encoded or not depending
570 // on the class loader
571 // This is definitely not as clean as using JAR URLs either
572 // over file or the custom jndi handler, but a lot less
573 // buggy overall
574
575 // Check that the URL is using file protocol, else ignore it
576 if (!"file".equals(urls[i].getProtocol())) {
577 continue;
578 }
579
580 File file = null;
581 try {
582 file = new File(urls[i].toURI());
583 } catch (URISyntaxException e) {
584 // Ignore, probably an unencoded char
585 file = new File(urls[i].getFile());
586 }
587 try {
588 file = file.getCanonicalFile();
589 } catch (IOException e) {
590 // Ignore
591 }
592 if (!file.exists()) {
593 continue;
594 }
595 String path = file.getAbsolutePath();
596 if (!path.endsWith(".jar")) {
597 continue;
598 }
599 /*
600 * Scan all JARs from WEB-INF/lib, plus any shared JARs
601 * that are not known not to contain any TLDs
602 */
603 if (loader == webappLoader
604 || noTldJars == null
605 || !noTldJars.contains(file.getName())) {
606 if (jarPathMap == null) {
607 jarPathMap = new HashMap();
608 jarPathMap.put(path, file);
609 } else if (!jarPathMap.containsKey(path)) {
610 jarPathMap.put(path, file);
611 }
612 }
613 }
614 }
615 loader = loader.getParent();
616 }
617
618 return jarPathMap;
619 }
620
621 public void lifecycleEvent(LifecycleEvent event) {
622 // Identify the context we are associated with
623 try {
624 context = (Context) event.getLifecycle();
625 } catch (ClassCastException e) {
626 log.error(sm.getString("tldConfig.cce", event.getLifecycle()), e);
627 return;
628 }
629
630 if (event.getType().equals(Lifecycle.INIT_EVENT)) {
631 init();
632 } else if (event.getType().equals(Lifecycle.START_EVENT)) {
633 try {
634 execute();
635 } catch (Exception e) {
636 log.error(sm.getString(
637 "tldConfig.execute", context.getPath()), e);
638 }
639 } else if (event.getType().equals(Lifecycle.STOP_EVENT)) {
640 listeners.clear();
641 }
642 }
643
644 private void init() {
645 if (tldDigester == null){
646 // (1) check if the attribute has been defined
647 // on the context element.
648 setTldValidation(context.getTldValidation());
649 setTldNamespaceAware(context.getTldNamespaceAware());
650
651 // (2) if the attribute wasn't defined on the context
652 // try the host.
653 if (!tldValidation) {
654 setTldValidation(
655 ((StandardHost) context.getParent()).getXmlValidation());
656 }
657
658 if (!tldNamespaceAware) {
659 setTldNamespaceAware(
660 ((StandardHost) context.getParent()).getXmlNamespaceAware());
661 }
662
663 tldDigester = DigesterFactory.newDigester(tldValidation,
664 tldNamespaceAware,
665 new TldRuleSet());
666 tldDigester.getParser();
667 }
668 }
669 }