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