View Javadoc

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  package org.apache.struts2.jasper.compiler;
19  
20  import com.opensymphony.xwork2.util.logging.Logger;
21  import com.opensymphony.xwork2.util.logging.LoggerFactory;
22  import org.apache.commons.io.FileUtils;
23  import org.apache.struts2.jasper.Constants;
24  import org.apache.struts2.jasper.JasperException;
25  import org.apache.struts2.jasper.xmlparser.ParserUtils;
26  import org.apache.struts2.jasper.xmlparser.TreeNode;
27  import org.xml.sax.InputSource;
28  
29  import javax.servlet.ServletContext;
30  import java.io.File;
31  import java.io.InputStream;
32  import java.net.*;
33  import java.util.*;
34  import java.util.jar.JarEntry;
35  import java.util.jar.JarFile;
36  
37  /***
38   * A container for all tag libraries that are defined "globally"
39   * for the web application.
40   * <p/>
41   * Tag Libraries can be defined globally in one of two ways:
42   * 1. Via <taglib> elements in web.xml:
43   * the uri and location of the tag-library are specified in
44   * the <taglib> element.
45   * 2. Via packaged jar files that contain .tld files
46   * within the META-INF directory, or some subdirectory
47   * of it. The taglib is 'global' if it has the <uri>
48   * element defined.
49   * <p/>
50   * A mapping between the taglib URI and its associated TaglibraryInfoImpl
51   * is maintained in this container.
52   * Actually, that's what we'd like to do. However, because of the
53   * way the classes TagLibraryInfo and TagInfo have been defined,
54   * it is not currently possible to share an instance of TagLibraryInfo
55   * across page invocations. A bug has been submitted to the spec lead.
56   * In the mean time, all we do is save the 'location' where the
57   * TLD associated with a taglib URI can be found.
58   * <p/>
59   * When a JSP page has a taglib directive, the mappings in this container
60   * are first searched (see method getLocation()).
61   * If a mapping is found, then the location of the TLD is returned.
62   * If no mapping is found, then the uri specified
63   * in the taglib directive is to be interpreted as the location for
64   * the TLD of this tag library.
65   *
66   * @author Pierre Delisle
67   * @author Jan Luehe
68   */
69  
70  public class TldLocationsCache {
71  
72      // Logger
73      private Logger log = LoggerFactory.getLogger(TldLocationsCache.class);
74  
75      /***
76       * The types of URI one may specify for a tag library
77       */
78      public static final int ABS_URI = 0;
79      public static final int ROOT_REL_URI = 1;
80      public static final int NOROOT_REL_URI = 2;
81  
82      private static final String WEB_XML = "/WEB-INF/web.xml";
83      private static final String FILE_PROTOCOL = "file:";
84      private static final String JAR_FILE_SUFFIX = ".jar";
85  
86      // Names of JARs that are known not to contain any TLDs
87      private static HashSet noTldJars;
88  
89      /***
90       * The mapping of the 'global' tag library URI to the location (resource
91       * path) of the TLD associated with that tag library. The location is
92       * returned as a String array:
93       * [0] The location
94       * [1] If the location is a jar file, this is the location of the tld.
95       */
96      private Hashtable mappings;
97  
98      private boolean initialized;
99      private ServletContext ctxt;
100     private boolean redeployMode;
101 
102     //**********************************************************************
103     // Constructor and Initilizations
104 
105     /*
106      * Initializes the set of JARs that are known not to contain any TLDs
107      */
108 
109     static {
110         noTldJars = new HashSet();
111         noTldJars.add("ant.jar");
112         noTldJars.add("catalina.jar");
113         noTldJars.add("catalina-ant.jar");
114         noTldJars.add("catalina-cluster.jar");
115         noTldJars.add("catalina-optional.jar");
116         noTldJars.add("catalina-i18n-fr.jar");
117         noTldJars.add("catalina-i18n-ja.jar");
118         noTldJars.add("catalina-i18n-es.jar");
119         noTldJars.add("commons-dbcp.jar");
120         noTldJars.add("commons-modeler.jar");
121         noTldJars.add("commons-logging-api.jar");
122         noTldJars.add("commons-beanutils.jar");
123         noTldJars.add("commons-fileupload-1.0.jar");
124         noTldJars.add("commons-pool.jar");
125         noTldJars.add("commons-digester.jar");
126         noTldJars.add("commons-logging.jar");
127         noTldJars.add("commons-collections.jar");
128         noTldJars.add("commons-el.jar");
129         noTldJars.add("jakarta-regexp-1.2.jar");
130         noTldJars.add("jasper-compiler.jar");
131         noTldJars.add("jasper-runtime.jar");
132         noTldJars.add("jmx.jar");
133         noTldJars.add("jmx-tools.jar");
134         noTldJars.add("jsp-api.jar");
135         noTldJars.add("jkshm.jar");
136         noTldJars.add("jkconfig.jar");
137         noTldJars.add("naming-common.jar");
138         noTldJars.add("naming-resources.jar");
139         noTldJars.add("naming-factory.jar");
140         noTldJars.add("naming-java.jar");
141         noTldJars.add("servlet-api.jar");
142         noTldJars.add("servlets-default.jar");
143         noTldJars.add("servlets-invoker.jar");
144         noTldJars.add("servlets-common.jar");
145         noTldJars.add("servlets-webdav.jar");
146         noTldJars.add("tomcat-util.jar");
147         noTldJars.add("tomcat-http11.jar");
148         noTldJars.add("tomcat-jni.jar");
149         noTldJars.add("tomcat-jk.jar");
150         noTldJars.add("tomcat-jk2.jar");
151         noTldJars.add("tomcat-coyote.jar");
152         noTldJars.add("xercesImpl.jar");
153         noTldJars.add("xmlParserAPIs.jar");
154         noTldJars.add("xml-apis.jar");
155         // JARs from J2SE runtime
156         noTldJars.add("sunjce_provider.jar");
157         noTldJars.add("ldapsec.jar");
158         noTldJars.add("localedata.jar");
159         noTldJars.add("dnsns.jar");
160     }
161 
162     public TldLocationsCache(ServletContext ctxt) {
163         this(ctxt, true);
164     }
165 
166     /***
167      * Constructor.
168      *
169      * @param ctxt         the servlet context of the web application in which Jasper
170      *                     is running
171      * @param redeployMode if true, then the compiler will allow redeploying
172      *                     a tag library from the same jar, at the expense of slowing down the
173      *                     server a bit. Note that this may only work on JDK 1.3.1_01a and later,
174      *                     because of JDK bug 4211817 fixed in this release.
175      *                     If redeployMode is false, a faster but less capable mode will be used.
176      */
177     public TldLocationsCache(ServletContext ctxt, boolean redeployMode) {
178         this.ctxt = ctxt;
179         this.redeployMode = redeployMode;
180         mappings = new Hashtable();
181         initialized = false;
182     }
183 
184     /***
185      * Sets the list of JARs that are known not to contain any TLDs.
186      *
187      * @param jarNames List of comma-separated names of JAR files that are
188      *                 known not to contain any TLDs
189      */
190     public static void setNoTldJars(String jarNames) {
191         if (jarNames != null) {
192             noTldJars.clear();
193             StringTokenizer tokenizer = new StringTokenizer(jarNames, ",");
194             while (tokenizer.hasMoreElements()) {
195                 noTldJars.add(tokenizer.nextToken());
196             }
197         }
198     }
199 
200     /***
201      * Gets the 'location' of the TLD associated with the given taglib 'uri'.
202      * <p/>
203      * Returns null if the uri is not associated with any tag library 'exposed'
204      * in the web application. A tag library is 'exposed' either explicitly in
205      * web.xml or implicitly via the uri tag in the TLD of a taglib deployed
206      * in a jar file (WEB-INF/lib).
207      *
208      * @param uri The taglib uri
209      * @return An array of two Strings: The first element denotes the real
210      *         path to the TLD. If the path to the TLD points to a jar file, then the
211      *         second element denotes the name of the TLD entry in the jar file.
212      *         Returns null if the uri is not associated with any tag library 'exposed'
213      *         in the web application.
214      */
215     public String[] getLocation(String uri) throws JasperException {
216         if (!initialized) {
217             init();
218         }
219         return (String[]) mappings.get(uri);
220     }
221 
222     /***
223      * Returns a list of absolute paths of the locations in the cache
224      */
225     public Set<String> getAbsolutePathsOfLocations() {
226         Set<String> paths = new HashSet<String>(mappings.size());
227         for (Object value : mappings.values()) {
228             String[] location = (String[]) value;
229 
230             try {
231                 File file = FileUtils.toFile(new URL(location[0]));
232                 paths.add(file.getAbsolutePath());
233             } catch (Exception e) {
234                 //ignore
235             }
236         }
237         return paths;
238     }
239 
240     /***
241      * Returns the type of a URI:
242      * ABS_URI
243      * ROOT_REL_URI
244      * NOROOT_REL_URI
245      */
246     public static int uriType(String uri) {
247         if (uri.indexOf(':') != -1) {
248             return ABS_URI;
249         } else if (uri.startsWith("/")) {
250             return ROOT_REL_URI;
251         } else {
252             return NOROOT_REL_URI;
253         }
254     }
255 
256     private void init() throws JasperException {
257         if (initialized) return;
258         try {
259             processWebDotXml();
260             scanJars();
261             processTldsInFileSystem("/WEB-INF/");
262             initialized = true;
263         } catch (Exception ex) {
264             throw new JasperException(Localizer.getMessage(
265                     "jsp.error.internal.tldinit", ex.getMessage()));
266         }
267     }
268 
269     /*
270      * Populates taglib map described in web.xml.
271      */
272     private void processWebDotXml() throws Exception {
273 
274         InputStream is = null;
275 
276         try {
277             // Acquire input stream to web application deployment descriptor
278             String altDDName = (String) ctxt.getAttribute(
279                     Constants.ALT_DD_ATTR);
280             URL uri = null;
281             if (altDDName != null) {
282                 try {
283                     uri = new URL(FILE_PROTOCOL + altDDName.replace('//', '/'));
284                 } catch (MalformedURLException e) {
285                     if (log.isWarnEnabled()) {
286                         log.warn(Localizer.getMessage(
287                                 "jsp.error.internal.filenotfound",
288                                 altDDName));
289                     }
290                 }
291             } else {
292                 uri = ctxt.getResource(WEB_XML);
293             }
294 
295             if (uri == null) {
296                 return;
297             }
298             is = uri.openStream();
299             InputSource ip = new InputSource(is);
300             ip.setSystemId(uri.toExternalForm());
301 
302             // Parse the web application deployment descriptor
303             TreeNode webtld = null;
304             // altDDName is the absolute path of the DD
305             if (altDDName != null) {
306                 webtld = new ParserUtils().parseXMLDocument(altDDName, ip);
307             } else {
308                 webtld = new ParserUtils().parseXMLDocument(WEB_XML, ip);
309             }
310 
311             // Allow taglib to be an element of the root or jsp-config (JSP2.0)
312             TreeNode jspConfig = webtld.findChild("jsp-config");
313             if (jspConfig != null) {
314                 webtld = jspConfig;
315             }
316             Iterator taglibs = webtld.findChildren("taglib");
317             while (taglibs.hasNext()) {
318 
319                 // Parse the next <taglib> element
320                 TreeNode taglib = (TreeNode) taglibs.next();
321                 String tagUri = null;
322                 String tagLoc = null;
323                 TreeNode child = taglib.findChild("taglib-uri");
324                 if (child != null)
325                     tagUri = child.getBody();
326                 child = taglib.findChild("taglib-location");
327                 if (child != null)
328                     tagLoc = child.getBody();
329 
330                 // Save this location if appropriate
331                 if (tagLoc == null)
332                     continue;
333                 if (uriType(tagLoc) == NOROOT_REL_URI)
334                     tagLoc = "/WEB-INF/" + tagLoc;
335                 String tagLoc2 = null;
336                 if (tagLoc.endsWith(JAR_FILE_SUFFIX)) {
337                     tagLoc = ctxt.getResource(tagLoc).toString();
338                     tagLoc2 = "META-INF/taglib.tld";
339                 }
340                 mappings.put(tagUri, new String[]{tagLoc, tagLoc2});
341             }
342         } finally {
343             if (is != null) {
344                 try {
345                     is.close();
346                 } catch (Throwable t) {
347                 }
348             }
349         }
350     }
351 
352     /***
353      * Scans the given JarURLConnection for TLD files located in META-INF
354      * (or a subdirectory of it), adding an implicit map entry to the taglib
355      * map for any TLD that has a <uri> element.
356      *
357      * @param conn   The JarURLConnection to the JAR file to scan
358      * @param ignore true if any exceptions raised when processing the given
359      *               JAR should be ignored, false otherwise
360      */
361     private void scanJar(JarURLConnection conn, boolean ignore)
362             throws JasperException {
363 
364         JarFile jarFile = null;
365         String resourcePath = conn.getJarFileURL().toString();
366         try {
367             if (redeployMode) {
368                 conn.setUseCaches(false);
369             }
370             jarFile = conn.getJarFile();
371             Enumeration entries = jarFile.entries();
372             while (entries.hasMoreElements()) {
373                 JarEntry entry = (JarEntry) entries.nextElement();
374                 String name = entry.getName();
375                 if (!name.startsWith("META-INF/")) continue;
376                 if (!name.endsWith(".tld")) continue;
377                 InputStream stream = jarFile.getInputStream(entry);
378                 try {
379                     String uri = getUriFromTld(resourcePath, stream);
380                     // Add implicit map entry only if its uri is not already
381                     // present in the map
382                     if (uri != null && mappings.get(uri) == null) {
383                         mappings.put(uri, new String[]{resourcePath, name});
384                     }
385                 } finally {
386                     if (stream != null) {
387                         try {
388                             stream.close();
389                         } catch (Throwable t) {
390                             // do nothing
391                         }
392                     }
393                 }
394             }
395         } catch (Exception ex) {
396             if (!redeployMode) {
397                 // if not in redeploy mode, close the jar in case of an error
398                 if (jarFile != null) {
399                     try {
400                         jarFile.close();
401                     } catch (Throwable t) {
402                         // ignore
403                     }
404                 }
405             }
406             if (!ignore) {
407                 throw new JasperException(ex);
408             }
409         } finally {
410             if (redeployMode) {
411                 // if in redeploy mode, always close the jar
412                 if (jarFile != null) {
413                     try {
414                         jarFile.close();
415                     } catch (Throwable t) {
416                         // ignore
417                     }
418                 }
419             }
420         }
421     }
422 
423     /*
424      * Searches the filesystem under /WEB-INF for any TLD files, and adds
425      * an implicit map entry to the taglib map for any TLD that has a <uri>
426      * element.
427      */
428     private void processTldsInFileSystem(String startPath)
429             throws Exception {
430 
431         Set dirList = ctxt.getResourcePaths(startPath);
432         if (dirList != null) {
433             Iterator it = dirList.iterator();
434             while (it.hasNext()) {
435                 String path = (String) it.next();
436                 if (path.endsWith("/")) {
437                     processTldsInFileSystem(path);
438                 }
439                 if (!path.endsWith(".tld")) {
440                     continue;
441                 }
442                 InputStream stream = ctxt.getResourceAsStream(path);
443                 String uri = null;
444                 try {
445                     uri = getUriFromTld(path, stream);
446                 } finally {
447                     if (stream != null) {
448                         try {
449                             stream.close();
450                         } catch (Throwable t) {
451                             // do nothing
452                         }
453                     }
454                 }
455                 // Add implicit map entry only if its uri is not already
456                 // present in the map
457                 if (uri != null && mappings.get(uri) == null) {
458                     mappings.put(uri, new String[]{path, null});
459                 }
460             }
461         }
462     }
463 
464     /*
465      * Returns the value of the uri element of the given TLD, or null if the
466      * given TLD does not contain any such element.
467      */
468     private String getUriFromTld(String resourcePath, InputStream in)
469             throws JasperException {
470         // Parse the tag library descriptor at the specified resource path
471         TreeNode tld = new ParserUtils().parseXMLDocument(resourcePath, in);
472         TreeNode uri = tld.findChild("uri");
473         if (uri != null) {
474             String body = uri.getBody();
475             if (body != null)
476                 return body;
477         }
478 
479         return null;
480     }
481 
482     /*
483      * Scans all JARs accessible to the webapp's classloader and its
484      * parent classloaders for TLDs.
485      * 
486      * The list of JARs always includes the JARs under WEB-INF/lib, as well as
487      * all shared JARs in the classloader delegation chain of the webapp's
488      * classloader.
489      *
490      * Considering JARs in the classloader delegation chain constitutes a
491      * Tomcat-specific extension to the TLD search
492      * order defined in the JSP spec. It allows tag libraries packaged as JAR
493      * files to be shared by web applications by simply dropping them in a 
494      * location that all web applications have access to (e.g.,
495      * <CATALINA_HOME>/common/lib).
496      *
497      * The set of shared JARs to be scanned for TLDs is narrowed down by
498      * the <tt>noTldJars</tt> class variable, which contains the names of JARs
499      * that are known not to contain any TLDs.
500      */
501     private void scanJars() throws Exception {
502 
503         ClassLoader webappLoader
504                 = Thread.currentThread().getContextClassLoader();
505         ClassLoader loader = webappLoader;
506 
507         while (loader != null) {
508             if (loader instanceof URLClassLoader) {
509                 URL[] urls = ((URLClassLoader) loader).getURLs();
510                 for (int i = 0; i < urls.length; i++) {
511                     URLConnection conn = urls[i].openConnection();
512                     if (conn instanceof JarURLConnection) {
513                         if (needScanJar(loader, webappLoader,
514                                 ((JarURLConnection) conn).getJarFile().getName())) {
515                             scanJar((JarURLConnection) conn, true);
516                         }
517                     } else {
518                         String urlStr = urls[i].toString();
519                         if (urlStr.startsWith(FILE_PROTOCOL)
520                                 && urlStr.endsWith(JAR_FILE_SUFFIX)
521                                 && needScanJar(loader, webappLoader, urlStr)) {
522                             URL jarURL = new URL("jar:" + urlStr + "!/");
523                             scanJar((JarURLConnection) jarURL.openConnection(),
524                                     true);
525                         }
526                     }
527                 }
528             }
529 
530             loader = loader.getParent();
531         }
532     }
533 
534     /*
535      * Determines if the JAR file with the given <tt>jarPath</tt> needs to be
536      * scanned for TLDs.
537      *
538      * @param loader The current classloader in the parent chain
539      * @param webappLoader The webapp classloader
540      * @param jarPath The JAR file path
541      *
542      * @return TRUE if the JAR file identified by <tt>jarPath</tt> needs to be
543      * scanned for TLDs, FALSE otherwise
544      */
545     private boolean needScanJar(ClassLoader loader, ClassLoader webappLoader,
546                                 String jarPath) {
547         if (loader == webappLoader) {
548             // JARs under WEB-INF/lib must be scanned unconditionally according
549             // to the spec.
550             return true;
551         } else {
552             String jarName = jarPath;
553             int slash = jarPath.lastIndexOf('/');
554             if (slash >= 0) {
555                 jarName = jarPath.substring(slash + 1);
556             }
557             return (!noTldJars.contains(jarName));
558         }
559     }
560 }