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