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