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