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.geronimo.jasper;
019    
020    import java.io.InputStream;
021    import java.net.JarURLConnection;
022    import java.net.MalformedURLException;
023    import java.net.URL;
024    import java.net.URLClassLoader;
025    import java.net.URLConnection;
026    import java.util.ArrayList;
027    import java.util.Enumeration;
028    import java.util.Hashtable;
029    import java.util.HashSet;
030    import java.util.Iterator;
031    import java.util.Set;
032    import java.util.StringTokenizer;
033    import java.util.jar.JarEntry;
034    import java.util.jar.JarFile;
035    import org.xml.sax.InputSource;
036    
037    import javax.servlet.ServletContext;
038    
039    import org.apache.geronimo.kernel.config.MultiParentClassLoader;
040    import org.apache.jasper.Constants;
041    import org.apache.jasper.JasperException;
042    import org.apache.jasper.compiler.TldLocationsCache;
043    import org.apache.jasper.xmlparser.ParserUtils;
044    import org.apache.jasper.xmlparser.TreeNode;
045    import org.apache.juli.logging.Log;
046    import org.apache.juli.logging.LogFactory;
047    
048    /**
049     * A container for all tag libraries that are defined "globally"
050     * for the web application.
051     * 
052     * Tag Libraries can be defined globally in one of two ways:
053     *   1. Via <taglib> elements in web.xml:
054     *      the uri and location of the tag-library are specified in
055     *      the <taglib> element.
056     *   2. Via packaged jar files that contain .tld files
057     *      within the META-INF directory, or some subdirectory
058     *      of it. The taglib is 'global' if it has the <uri>
059     *      element defined.
060     *
061     * A mapping between the taglib URI and its associated TaglibraryInfoImpl
062     * is maintained in this container.
063     * Actually, that's what we'd like to do. However, because of the
064     * way the classes TagLibraryInfo and TagInfo have been defined,
065     * it is not currently possible to share an instance of TagLibraryInfo
066     * across page invocations. A bug has been submitted to the spec lead.
067     * In the mean time, all we do is save the 'location' where the
068     * TLD associated with a taglib URI can be found.
069     *
070     * When a JSP page has a taglib directive, the mappings in this container
071     * are first searched (see method getLocation()).
072     * If a mapping is found, then the location of the TLD is returned.
073     * If no mapping is found, then the uri specified
074     * in the taglib directive is to be interpreted as the location for
075     * the TLD of this tag library.
076     * 
077     * This class was copied from tomcat to allow Geronimo
078     * to override Jasper's default TldLocationsCache which does not work
079     * with Geronimo's MultiParentClassLoader.  Copying was necessary because
080     * most of the essential methods and member variables were private.
081     *
082     * @author Pierre Delisle
083     * @author Jan Luehe
084     */
085    
086    public class GeronimoTldLocationsCache extends TldLocationsCache {
087    
088        // Logger
089        private Log log = LogFactory.getLog(GeronimoTldLocationsCache.class);
090    
091        /**
092         * The types of URI one may specify for a tag library
093         */
094        public static final int ABS_URI = 0;
095        public static final int ROOT_REL_URI = 1;
096        public static final int NOROOT_REL_URI = 2;
097    
098        private static final String WEB_XML = "/WEB-INF/web.xml";
099        private static final String FILE_PROTOCOL = "file:";
100        private static final String JAR_FILE_SUFFIX = ".jar";
101    
102        // Names of JARs that are known not to contain any TLDs
103        private static HashSet<String> noTldJars;
104        
105        // Names of JARs that have already been scanned
106        ArrayList<String> scannedJars;
107        
108        /**
109         * The mapping of the 'global' tag library URI to the location (resource
110         * path) of the TLD associated with that tag library. The location is
111         * returned as a String array:
112         *    [0] The location
113         *    [1] If the location is a jar file, this is the location of the tld.
114         */
115        private Hashtable<String,String[]> mappings;
116    
117        private boolean initialized;
118        private ServletContext ctxt;
119        private boolean redeployMode;
120    
121        //*********************************************************************
122        // Constructor and Initilizations
123    
124        /*
125         * Initializes the set of JARs that are known not to contain any TLDs
126         */
127        static {
128            noTldJars = new HashSet<String>();
129            // Misc JARs not included with Tomcat
130            noTldJars.add("ant.jar");
131            noTldJars.add("commons-dbcp.jar");
132            noTldJars.add("commons-beanutils.jar");
133            noTldJars.add("commons-fileupload-1.0.jar");
134            noTldJars.add("commons-pool.jar");
135            noTldJars.add("commons-digester.jar");
136            noTldJars.add("commons-logging.jar");
137            noTldJars.add("commons-collections.jar");
138            noTldJars.add("jmx.jar");
139            noTldJars.add("jmx-tools.jar");
140            noTldJars.add("xercesImpl.jar");
141            noTldJars.add("xmlParserAPIs.jar");
142            noTldJars.add("xml-apis.jar");
143            // JARs from J2SE runtime
144            noTldJars.add("sunjce_provider.jar");
145            noTldJars.add("ldapsec.jar");
146            noTldJars.add("localedata.jar");
147            noTldJars.add("dnsns.jar");
148            noTldJars.add("tools.jar");
149            noTldJars.add("sunpkcs11.jar");
150        }
151        
152        public GeronimoTldLocationsCache(ServletContext ctxt) {
153            this(ctxt, true);
154        }
155    
156        /** Constructor. 
157         *
158         * @param ctxt the servlet context of the web application in which Jasper 
159         * is running
160         * @param redeployMode if true, then the compiler will allow redeploying 
161         * a tag library from the same jar, at the expense of slowing down the
162         * server a bit. Note that this may only work on JDK 1.3.1_01a and later,
163         * because of JDK bug 4211817 fixed in this release.
164         * If redeployMode is false, a faster but less capable mode will be used.
165         */
166        public GeronimoTldLocationsCache(ServletContext ctxt, boolean redeployMode) {
167            super(ctxt,redeployMode);
168            scannedJars = new ArrayList<String>();
169            this.ctxt = ctxt;
170            this.redeployMode = redeployMode;
171            mappings = new Hashtable<String,String[]>();
172            initialized = false;        
173        }
174    
175        /**
176         * Sets the list of JARs that are known not to contain any TLDs.
177         *
178         * @param jarNames List of comma-separated names of JAR files that are 
179         * known not to contain any TLDs 
180         */
181        public static void setNoTldJars(String jarNames) {
182            if (jarNames != null) {
183                noTldJars.clear();
184                StringTokenizer tokenizer = new StringTokenizer(jarNames, ",");
185                while (tokenizer.hasMoreElements()) {
186                    noTldJars.add(tokenizer.nextToken());
187                }
188            }
189        }
190    
191        /**
192         * Gets the 'location' of the TLD associated with the given taglib 'uri'.
193         *
194         * Returns null if the uri is not associated with any tag library 'exposed'
195         * in the web application. A tag library is 'exposed' either explicitly in
196         * web.xml or implicitly via the uri tag in the TLD of a taglib deployed
197         * in a jar file (WEB-INF/lib).
198         * 
199         * @param uri The taglib uri
200         *
201         * @return An array of two Strings: The first element denotes the real
202         * path to the TLD. If the path to the TLD points to a jar file, then the
203         * second element denotes the name of the TLD entry in the jar file.
204         * Returns null if the uri is not associated with any tag library 'exposed'
205         * in the web application.
206         */
207        public String[] getLocation(String uri) throws JasperException {
208            if (!initialized) {
209                init();
210            }
211            return (String[]) mappings.get(uri);
212        }
213    
214        private void init() throws JasperException {
215            if (initialized) return;
216            try {
217                processWebDotXml();
218                scanJars(Thread.currentThread().getContextClassLoader());
219                processTldsInFileSystem("/WEB-INF/");
220                initialized = true;
221            } catch (Exception ex) {
222                throw new JasperException(ex);
223            }
224        }
225    
226        /*
227         * Populates taglib map described in web.xml.
228         */    
229        private void processWebDotXml() throws Exception {
230    
231            InputStream is = null;
232    
233            try {
234                // Acquire input stream to web application deployment descriptor
235                String altDDName = (String)ctxt.getAttribute(
236                                                        Constants.ALT_DD_ATTR);
237                URL uri = null;
238                if (altDDName != null) {
239                    try {
240                        uri = new URL(FILE_PROTOCOL+altDDName.replace('\\', '/'));
241                    } catch (MalformedURLException e) {
242                        if (log.isWarnEnabled()) {
243                            log.warn("file not found: " + altDDName);
244                        }
245                    }
246                } else {
247                    uri = ctxt.getResource(WEB_XML);
248                    if (uri == null && log.isWarnEnabled()) {
249                        log.warn("file not found: " + WEB_XML);
250                    }
251                }
252    
253                if (uri == null) {
254                    return;
255                }
256                is = uri.openStream();
257                InputSource ip = new InputSource(is);
258                ip.setSystemId(uri.toExternalForm()); 
259    
260                // Parse the web application deployment descriptor
261                TreeNode webtld = null;
262                // altDDName is the absolute path of the DD
263                if (altDDName != null) {
264                    webtld = new ParserUtils().parseXMLDocument(altDDName, ip);
265                } else {
266                    webtld = new ParserUtils().parseXMLDocument(WEB_XML, ip);
267                }
268    
269                // Allow taglib to be an element of the root or jsp-config (JSP2.0)
270                TreeNode jspConfig = webtld.findChild("jsp-config");
271                if (jspConfig != null) {
272                    webtld = jspConfig;
273                }
274                Iterator taglibs = webtld.findChildren("taglib");
275                while (taglibs.hasNext()) {
276    
277                    // Parse the next <taglib> element
278                    TreeNode taglib = (TreeNode) taglibs.next();
279                    String tagUri = null;
280                    String tagLoc = null;
281                    TreeNode child = taglib.findChild("taglib-uri");
282                    if (child != null)
283                        tagUri = child.getBody();
284                    child = taglib.findChild("taglib-location");
285                    if (child != null)
286                        tagLoc = child.getBody();
287    
288                    // Save this location if appropriate
289                    if (tagLoc == null)
290                        continue;
291                    if (uriType(tagLoc) == NOROOT_REL_URI)
292                        tagLoc = "/WEB-INF/" + tagLoc;
293                    String tagLoc2 = null;
294                    if (tagLoc.endsWith(JAR_FILE_SUFFIX)) {
295                        tagLoc = ctxt.getResource(tagLoc).toString();
296                        tagLoc2 = "META-INF/taglib.tld";
297                    }
298                    mappings.put(tagUri, new String[] { tagLoc, tagLoc2 });
299                }
300            } finally {
301                if (is != null) {
302                    try {
303                        is.close();
304                    } catch (Throwable t) {}
305                }
306            }
307        }
308    
309        /**
310         * Scans the given JarURLConnection for TLD files located in META-INF
311         * (or a subdirectory of it), adding an implicit map entry to the taglib
312         * map for any TLD that has a <uri> element.
313         *
314         * @param conn The JarURLConnection to the JAR file to scan
315         * @param ignore true if any exceptions raised when processing the given
316         * JAR should be ignored, false otherwise
317         */
318        private void scanJar(JarURLConnection conn, boolean ignore)
319                    throws JasperException {
320    
321            JarFile jarFile = null;
322            String resourcePath = conn.getJarFileURL().toString();
323            try {
324                if (redeployMode) {
325                    conn.setUseCaches(false);
326                }
327                jarFile = conn.getJarFile();
328                Enumeration entries = jarFile.entries();
329                while (entries.hasMoreElements()) {
330                    JarEntry entry = (JarEntry) entries.nextElement();
331                    String name = entry.getName();
332                    if (!name.startsWith("META-INF/")) continue;
333                    if (!name.endsWith(".tld")) continue;
334                    InputStream stream = jarFile.getInputStream(entry);
335                    try {
336                        String uri = getUriFromTld(resourcePath, stream);
337                        // Add implicit map entry only if its uri is not already
338                        // present in the map
339                        if (uri != null && mappings.get(uri) == null) {
340                            mappings.put(uri, new String[]{ resourcePath, name });
341                        }
342                    } finally {
343                        if (stream != null) {
344                            try {
345                                stream.close();
346                            } catch (Throwable t) {
347                                // do nothing
348                            }
349                        }
350                    }
351                }
352            } catch (Exception ex) {
353                if (!redeployMode) {
354                    // if not in redeploy mode, close the jar in case of an error
355                    if (jarFile != null) {
356                        try {
357                            jarFile.close();
358                        } catch (Throwable t) {
359                            // ignore
360                        }
361                    }
362                }
363                if (!ignore) {
364                    throw new JasperException(ex);
365                }
366            } finally {
367                if (redeployMode) {
368                    // if in redeploy mode, always close the jar
369                    if (jarFile != null) {
370                        try {
371                            jarFile.close();
372                        } catch (Throwable t) {
373                            // ignore
374                        }
375                    }
376                }
377            }
378        }
379    
380        /*
381         * Searches the filesystem under /WEB-INF for any TLD files, and adds
382         * an implicit map entry to the taglib map for any TLD that has a <uri>
383         * element.
384         */
385        private void processTldsInFileSystem(String startPath)
386                throws Exception {
387    
388            Set dirList = ctxt.getResourcePaths(startPath);
389            if (dirList != null) {
390                Iterator it = dirList.iterator();
391                while (it.hasNext()) {
392                    String path = (String) it.next();
393                    if (path.endsWith("/")) {
394                        processTldsInFileSystem(path);
395                    }
396                    if (!path.endsWith(".tld")) {
397                        continue;
398                    }
399                    InputStream stream = ctxt.getResourceAsStream(path);
400                    String uri = null;
401                    try {
402                        uri = getUriFromTld(path, stream);
403                    } finally {
404                        if (stream != null) {
405                            try {
406                                stream.close();
407                            } catch (Throwable t) {
408                                // do nothing
409                            }
410                        }
411                    }
412                    // Add implicit map entry only if its uri is not already
413                    // present in the map
414                    if (uri != null && mappings.get(uri) == null) {
415                        mappings.put(uri, new String[] { path, null });
416                    }
417                }
418            }
419        }
420    
421        /*
422         * Returns the value of the uri element of the given TLD, or null if the
423         * given TLD does not contain any such element.
424         */
425        private String getUriFromTld(String resourcePath, InputStream in) 
426            throws JasperException
427        {
428            // Parse the tag library descriptor at the specified resource path
429            TreeNode tld = new ParserUtils().parseXMLDocument(resourcePath, in);
430            TreeNode uri = tld.findChild("uri");
431            if (uri != null) {
432                String body = uri.getBody();
433                if (body != null)
434                    return body;
435            }
436    
437            return null;
438        }
439    
440        /*
441         * Scans all JARs accessible to the webapp's classloader and its
442         * parent classloaders for TLDs.
443         * 
444         * The list of JARs always includes the JARs under WEB-INF/lib, as well as
445         * all shared JARs in the classloader delegation chain of the webapp's
446         * classloader.
447         *
448         * The set of shared JARs to be scanned for TLDs is narrowed down by
449         * the <tt>noTldJars</tt> class variable, which contains the names of JARs
450         * that are known not to contain any TLDs.
451         */
452        private void scanJars(ClassLoader loader) throws Exception {
453            
454            if (loader instanceof MultiParentClassLoader) {
455                MultiParentClassLoader mutliLoader = (MultiParentClassLoader) loader;
456                for (ClassLoader parent : mutliLoader.getParents()) {
457                    scanJars(parent);
458                }
459            }
460    
461            if (loader instanceof URLClassLoader) {
462                URL[] urls = ((URLClassLoader) loader).getURLs();
463                for (int i=0; i<urls.length; i++) {
464                    URLConnection conn = urls[i].openConnection();
465                    if (conn instanceof JarURLConnection) {
466                        if (needScanJar(loader,
467                                        ((JarURLConnection) conn).getJarFile().getName())) {
468                            scanJar((JarURLConnection) conn, true);
469                            scannedJars.add(((JarURLConnection) conn).getJarFile().getName());
470                        }
471                    } else {
472                        String urlStr = urls[i].toString();
473                        if (urlStr.startsWith(FILE_PROTOCOL)
474                                && urlStr.endsWith(JAR_FILE_SUFFIX)
475                                && needScanJar(loader, urlStr)) {
476                            URL jarURL = new URL("jar:" + urlStr + "!/");
477                            scanJar((JarURLConnection) jarURL.openConnection(),
478                                    true);
479                            scannedJars.add(urlStr);
480                        }
481                    }
482                }
483            }
484        }
485    
486        /*
487         * Determines if the JAR file with the given <tt>jarPath</tt> needs to be
488         * scanned for TLDs.
489         *
490         * @param loader The current classloader in the parent chain
491         * @param webappLoader The webapp classloader
492         * @param jarPath The JAR file path
493         *
494         * @return TRUE if the JAR file identified by <tt>jarPath</tt> needs to be
495         * scanned for TLDs, FALSE otherwise
496         */
497        private boolean needScanJar(ClassLoader loader,
498                                    String jarPath) {
499            if (scannedJars.contains(jarPath)) {
500                return false;
501            }
502            else if (loader == Thread.currentThread().getContextClassLoader()) {
503                // JARs under WEB-INF/lib must be scanned unconditionally according
504                // to the spec.
505                return true;
506            } else {
507                String jarName = jarPath;
508                int slash = jarPath.lastIndexOf('/');
509                if (slash >= 0) {
510                    jarName = jarPath.substring(slash + 1);
511                }
512                return (!noTldJars.contains(jarName));
513            }
514        }
515    }