blob: 4d6892f7c5c7b6b498a9663e4bc1180f404b2896 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.catalina.startup;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import javax.naming.NameClassPair;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.DirContext;
import javax.servlet.ServletException;
import org.apache.catalina.Context;
import org.apache.catalina.Globals;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.util.StringManager;
import org.apache.tomcat.util.digester.Digester;
import org.xml.sax.InputSource;
/**
* Startup event listener for a <b>Context</b> that configures the properties
* of that Context, and the associated defined servlets.
*
* @author Craig R. McClanahan
* @author Jean-Francois Arcand
* @author Costin Manolache
*/
public final class TldConfig {
// Names of JARs that are known not to contain any TLDs
private static HashSet noTldJars;
private static org.apache.commons.logging.Log log=
org.apache.commons.logging.LogFactory.getLog( TldConfig.class );
/*
* Initializes the set of JARs that are known not to contain any TLDs
*/
static {
noTldJars = new HashSet();
noTldJars.add("catalina.jar");
noTldJars.add("catalina-ant.jar");
noTldJars.add("catalina-cluster.jar");
noTldJars.add("catalina-optional.jar");
noTldJars.add("commons-el.jar");
noTldJars.add("commons-logging-api.jar");
noTldJars.add("commons-modeler.jar");
noTldJars.add("jasper-compiler.jar");
noTldJars.add("jasper-compiler-jdt.jar");
noTldJars.add("jasper-runtime.jar");
noTldJars.add("jsp-api.jar");
noTldJars.add("naming-resources.jar");
noTldJars.add("naming-factory.jar");
noTldJars.add("naming-factory-dbcp.jar");
noTldJars.add("servlet-api.jar");
noTldJars.add("servlets-cgi.jar");
noTldJars.add("servlets-default.jar");
noTldJars.add("servlets-invoker.jar");
noTldJars.add("servlets-ssi.jar");
noTldJars.add("servlets-webdav.jar");
noTldJars.add("tomcat-ajp.jar");
noTldJars.add("tomcat-coyote.jar");
noTldJars.add("tomcat-http.jar");
noTldJars.add("tomcat-util.jar");
// i18n JARs
noTldJars.add("catalina-i18n-en.jar");
noTldJars.add("catalina-i18n-es.jar");
noTldJars.add("catalina-i18n-fr.jar");
noTldJars.add("catalina-i18n-ja.jar");
// Misc JARs not included with Tomcat
noTldJars.add("ant.jar");
noTldJars.add("commons-dbcp.jar");
noTldJars.add("commons-beanutils.jar");
noTldJars.add("commons-fileupload-1.0.jar");
noTldJars.add("commons-pool.jar");
noTldJars.add("commons-digester.jar");
noTldJars.add("commons-logging.jar");
noTldJars.add("commons-collections.jar");
noTldJars.add("jmx.jar");
noTldJars.add("jmx-tools.jar");
noTldJars.add("xercesImpl.jar");
noTldJars.add("xmlParserAPIs.jar");
noTldJars.add("xml-apis.jar");
// JARs from J2SE runtime
noTldJars.add("sunjce_provider.jar");
noTldJars.add("ldapsec.jar");
noTldJars.add("localedata.jar");
noTldJars.add("dnsns.jar");
}
// ----------------------------------------------------- Instance Variables
/**
* The Context we are associated with.
*/
private Context context = null;
/**
* The string resources for this package.
*/
private static final StringManager sm =
StringManager.getManager(Constants.Package);
/**
* The <code>Digester</code> we will use to process tag library
* descriptor files.
*/
private static Digester tldDigester = null;
/**
* Attribute value used to turn on/off TLD validation
*/
private static boolean tldValidation = false;
/**
* Attribute value used to turn on/off TLD namespace awarenes.
*/
private static boolean tldNamespaceAware = false;
private boolean rescan=true;
private ArrayList listeners=new ArrayList();
// --------------------------------------------------------- Public Methods
/**
* Sets the list of JARs that are known not to contain any TLDs.
*
* @param jarNames List of comma-separated names of JAR files that are
* known not to contain any TLDs
*/
public static void setNoTldJars(String jarNames) {
if (jarNames != null) {
noTldJars.clear();
StringTokenizer tokenizer = new StringTokenizer(jarNames, ",");
while (tokenizer.hasMoreElements()) {
noTldJars.add(tokenizer.nextToken());
}
}
}
/**
* Set the validation feature of the XML parser used when
* parsing xml instances.
* @param tldValidation true to enable xml instance validation
*/
public void setTldValidation(boolean tldValidation){
TldConfig.tldValidation = tldValidation;
}
/**
* Get the server.xml <host> attribute's xmlValidation.
* @return true if validation is enabled.
*
*/
public boolean getTldValidation(){
return tldValidation;
}
/**
* Get the server.xml <host> attribute's xmlNamespaceAware.
* @return true if namespace awarenes is enabled.
*
*/
public boolean getTldNamespaceAware(){
return tldNamespaceAware;
}
/**
* Set the namespace aware feature of the XML parser used when
* parsing xml instances.
* @param tldNamespaceAware true to enable namespace awareness
*/
public void setTldNamespaceAware(boolean tldNamespaceAware){
TldConfig.tldNamespaceAware = tldNamespaceAware;
}
public boolean isRescan() {
return rescan;
}
public void setRescan(boolean rescan) {
this.rescan = rescan;
}
public Context getContext() {
return context;
}
public void setContext(Context context) {
this.context = context;
}
public void addApplicationListener( String s ) {
//if(log.isDebugEnabled())
log.debug( "Add tld listener " + s);
listeners.add(s);
}
public String[] getTldListeners() {
String result[]=new String[listeners.size()];
listeners.toArray(result);
return result;
}
/**
* Scan for and configure all tag library descriptors found in this
* web application.
*
* @exception Exception if a fatal input/output or parsing error occurs
*/
public void execute() throws Exception {
long t1=System.currentTimeMillis();
File tldCache=null;
if (context instanceof StandardContext) {
File workDir= (File)
((StandardContext)context).getServletContext().getAttribute(Globals.WORK_DIR_ATTR);
tldCache=new File( workDir, "tldCache.ser");
}
// Option to not rescan
if( ! rescan ) {
// find the cache
if( tldCache!= null && tldCache.exists()) {
// just read it...
processCache(tldCache);
return;
}
}
/*
* Acquire the list of TLD resource paths, possibly embedded in JAR
* files, to be processed
*/
Set resourcePaths = tldScanResourcePaths();
Map jarPaths = getJarPaths();
// Check to see if we can use cached listeners
if (tldCache != null && tldCache.exists()) {
long lastModified = getLastModified(resourcePaths, jarPaths);
if (lastModified < tldCache.lastModified()) {
processCache(tldCache);
return;
}
}
// Scan each accumulated resource path for TLDs to be processed
Iterator paths = resourcePaths.iterator();
while (paths.hasNext()) {
String path = (String) paths.next();
if (path.endsWith(".jar")) {
tldScanJar(path);
} else {
tldScanTld(path);
}
}
if (jarPaths != null) {
paths = jarPaths.values().iterator();
while (paths.hasNext()) {
tldScanJar((File) paths.next());
}
}
String list[] = getTldListeners();
if( tldCache!= null ) {
log.debug( "Saving tld cache: " + tldCache + " " + list.length);
try {
FileOutputStream out=new FileOutputStream(tldCache);
ObjectOutputStream oos=new ObjectOutputStream( out );
oos.writeObject( list );
oos.close();
} catch( IOException ex ) {
ex.printStackTrace();
}
}
if( log.isDebugEnabled() )
log.debug( "Adding tld listeners:" + list.length);
for( int i=0; list!=null && i<list.length; i++ ) {
context.addApplicationListener(list[i]);
}
long t2=System.currentTimeMillis();
if( context instanceof StandardContext ) {
((StandardContext)context).setTldScanTime(t2-t1);
}
}
// -------------------------------------------------------- Private Methods
/*
* Returns the last modification date of the given sets of resources.
*
* @param resourcePaths
* @param jarPaths
*
* @return Last modification date
*/
private long getLastModified(Set resourcePaths, Map jarPaths)
throws Exception {
long lastModified = 0;
Iterator paths = resourcePaths.iterator();
while (paths.hasNext()) {
String path = (String) paths.next();
URL url = context.getServletContext().getResource(path);
if (url == null) {
log.debug( "Null url "+ path );
break;
}
long lastM = url.openConnection().getLastModified();
if (lastM > lastModified) lastModified = lastM;
if (log.isDebugEnabled()) {
log.debug( "Last modified " + path + " " + lastM);
}
}
if (jarPaths != null) {
paths = jarPaths.values().iterator();
while (paths.hasNext()) {
File jarFile = (File) paths.next();
long lastM = jarFile.lastModified();
if (lastM > lastModified) lastModified = lastM;
if (log.isDebugEnabled()) {
log.debug("Last modified " + jarFile.getAbsolutePath()
+ " " + lastM);
}
}
}
return lastModified;
}
private void processCache(File tldCache ) throws IOException {
// read the cache and return;
try {
FileInputStream in=new FileInputStream(tldCache);
ObjectInputStream ois=new ObjectInputStream( in );
String list[]=(String [])ois.readObject();
if( log.isDebugEnabled() )
log.debug("Reusing tldCache " + tldCache + " " + list.length);
for( int i=0; list!=null && i<list.length; i++ ) {
context.addApplicationListener(list[i]);
}
ois.close();
} catch( ClassNotFoundException ex ) {
ex.printStackTrace();
}
}
/**
* Create (if necessary) and return a Digester configured to process a tag
* library descriptor, looking for additional listener classes to be
* registered.
*/
private static Digester createTldDigester() {
return DigesterFactory.newDigester(tldValidation,
tldNamespaceAware,
new TldRuleSet());
}
/**
* Scan the JAR file at the specified resource path for TLDs in the
* <code>META-INF</code> subdirectory, and scan each TLD for application
* event listeners that need to be registered.
*
* @param resourcePath Resource path of the JAR file to scan
*
* @exception Exception if an exception occurs while scanning this JAR
*/
private void tldScanJar(String resourcePath) throws Exception {
if (log.isDebugEnabled()) {
log.debug(" Scanning JAR at resource path '" + resourcePath + "'");
}
URL url = context.getServletContext().getResource(resourcePath);
if (url == null) {
throw new IllegalArgumentException
(sm.getString("contextConfig.tldResourcePath",
resourcePath));
}
File file = new File(url.getFile());
file = file.getCanonicalFile();
tldScanJar(file);
}
/**
* Scans all TLD entries in the given JAR for application listeners.
*
* @param file JAR file whose TLD entries are scanned for application
* listeners
*/
private void tldScanJar(File file) throws Exception {
JarFile jarFile = null;
String name = null;
String jarPath = file.getAbsolutePath();
try {
jarFile = new JarFile(file);
Enumeration entries = jarFile.entries();
while (entries.hasMoreElements()) {
JarEntry entry = (JarEntry) entries.nextElement();
name = entry.getName();
if (!name.startsWith("META-INF/")) {
continue;
}
if (!name.endsWith(".tld")) {
continue;
}
if (log.isTraceEnabled()) {
log.trace(" Processing TLD at '" + name + "'");
}
try {
tldScanStream(new InputSource(jarFile.getInputStream(entry)));
} catch (Exception e) {
log.error(sm.getString("contextConfig.tldEntryException",
name, jarPath, context.getPath()),
e);
}
}
} catch (Exception e) {
log.error(sm.getString("contextConfig.tldJarException",
jarPath, context.getPath()),
e);
} finally {
if (jarFile != null) {
try {
jarFile.close();
} catch (Throwable t) {
// Ignore
}
}
}
}
/**
* Scan the TLD contents in the specified input stream, and register
* any application event listeners found there. <b>NOTE</b> - It is
* the responsibility of the caller to close the InputStream after this
* method returns.
*
* @param resourceStream InputStream containing a tag library descriptor
*
* @exception Exception if an exception occurs while scanning this TLD
*/
private void tldScanStream(InputSource resourceStream)
throws Exception {
if (tldDigester == null){
tldDigester = createTldDigester();
}
synchronized (tldDigester) {
try {
tldDigester.push(this);
tldDigester.parse(resourceStream);
} finally {
tldDigester.reset();
}
}
}
/**
* Scan the TLD contents at the specified resource path, and register
* any application event listeners found there.
*
* @param resourcePath Resource path being scanned
*
* @exception Exception if an exception occurs while scanning this TLD
*/
private void tldScanTld(String resourcePath) throws Exception {
if (log.isDebugEnabled()) {
log.debug(" Scanning TLD at resource path '" + resourcePath + "'");
}
InputSource inputSource = null;
try {
// Bugzilla 38476
InputStream stream =
context.getServletContext().getResourceAsStream(resourcePath);
if (stream == null) {
throw new IllegalArgumentException
(sm.getString("contextConfig.tldResourcePath",
resourcePath));
}
inputSource = new InputSource(stream);
if (inputSource == null) {
throw new IllegalArgumentException
(sm.getString("contextConfig.tldResourcePath",
resourcePath));
}
tldScanStream(inputSource);
} catch (Exception e) {
throw new ServletException
(sm.getString("contextConfig.tldFileException", resourcePath,
context.getPath()),
e);
}
}
/**
* Accumulate and return a Set of resource paths to be analyzed for
* tag library descriptors. Each element of the returned set will be
* the context-relative path to either a tag library descriptor file,
* or to a JAR file that may contain tag library descriptors in its
* <code>META-INF</code> subdirectory.
*
* @exception IOException if an input/output error occurs while
* accumulating the list of resource paths
*/
private Set tldScanResourcePaths() throws IOException {
if (log.isDebugEnabled()) {
log.debug(" Accumulating TLD resource paths");
}
Set resourcePaths = new HashSet();
// Accumulate resource paths explicitly listed in the web application
// deployment descriptor
if (log.isTraceEnabled()) {
log.trace(" Scanning <taglib> elements in web.xml");
}
String taglibs[] = context.findTaglibs();
for (int i = 0; i < taglibs.length; i++) {
String resourcePath = context.findTaglib(taglibs[i]);
// FIXME - Servlet 2.4 DTD implies that the location MUST be
// a context-relative path starting with '/'?
if (!resourcePath.startsWith("/")) {
resourcePath = "/WEB-INF/" + resourcePath;
}
if (log.isTraceEnabled()) {
log.trace(" Adding path '" + resourcePath +
"' for URI '" + taglibs[i] + "'");
}
resourcePaths.add(resourcePath);
}
DirContext resources = context.getResources();
if (resources != null) {
tldScanResourcePathsWebInf(resources, "/WEB-INF", resourcePaths);
}
// Return the completed set
return (resourcePaths);
}
/*
* Scans the web application's subdirectory identified by rootPath,
* along with its subdirectories, for TLDs.
*
* Initially, rootPath equals /WEB-INF. The /WEB-INF/classes and
* /WEB-INF/lib subdirectories are excluded from the search, as per the
* JSP 2.0 spec.
*
* @param resources The web application's resources
* @param rootPath The path whose subdirectories are to be searched for
* TLDs
* @param tldPaths The set of TLD resource paths to add to
*/
private void tldScanResourcePathsWebInf(DirContext resources,
String rootPath,
Set tldPaths)
throws IOException {
if (log.isTraceEnabled()) {
log.trace(" Scanning TLDs in " + rootPath + " subdirectory");
}
try {
NamingEnumeration items = resources.list(rootPath);
while (items.hasMoreElements()) {
NameClassPair item = (NameClassPair) items.nextElement();
String resourcePath = rootPath + "/" + item.getName();
if (!resourcePath.endsWith(".tld")
&& (resourcePath.startsWith("/WEB-INF/classes")
|| resourcePath.startsWith("/WEB-INF/lib"))) {
continue;
}
if (resourcePath.endsWith(".tld")) {
if (log.isTraceEnabled()) {
log.trace(" Adding path '" + resourcePath + "'");
}
tldPaths.add(resourcePath);
} else {
tldScanResourcePathsWebInf(resources, resourcePath,
tldPaths);
}
}
} catch (NamingException e) {
; // Silent catch: it's valid that no /WEB-INF directory exists
}
}
/**
* Returns a map of the paths to all JAR files that are accessible to the
* webapp and will be scanned for TLDs.
*
* The map always includes all the JARs under WEB-INF/lib, as well as
* shared JARs in the classloader delegation chain of the webapp's
* classloader.
*
* The latter constitutes a Tomcat-specific extension to the TLD search
* order defined in the JSP spec. It allows tag libraries packaged as JAR
* files to be shared by web applications by simply dropping them in a
* location that all web applications have access to (e.g.,
* <CATALINA_HOME>/common/lib).
*
* The set of shared JARs to be scanned for TLDs is narrowed down by
* the <tt>noTldJars</tt> class variable, which contains the names of JARs
* that are known not to contain any TLDs.
*
* @return Map of JAR file paths
*/
private Map getJarPaths() {
HashMap jarPathMap = null;
ClassLoader webappLoader = Thread.currentThread().getContextClassLoader();
ClassLoader loader = webappLoader;
while (loader != null) {
if (loader instanceof URLClassLoader) {
URL[] urls = ((URLClassLoader) loader).getURLs();
for (int i=0; i<urls.length; i++) {
// Expect file URLs
// This is definitely not as clean as using JAR URLs either
// over file or the custom jndi handler, but a lot less
// buggy overall
File file;
try {
file = new File(new URI(urls[i].toString()).getPath());
} catch (URISyntaxException e1) {
continue;
}
try {
file = file.getCanonicalFile();
} catch (IOException e) {
// Ignore
}
if (!file.exists()) {
continue;
}
String path = file.getAbsolutePath();
if (!path.endsWith(".jar")) {
continue;
}
/*
* Scan all JARs from WEB-INF/lib, plus any shared JARs
* that are not known not to contain any TLDs
*/
if (loader == webappLoader
|| noTldJars == null
|| !noTldJars.contains(file.getName())) {
if (jarPathMap == null) {
jarPathMap = new HashMap();
jarPathMap.put(path, file);
} else if (!jarPathMap.containsKey(path)) {
jarPathMap.put(path, file);
}
}
}
}
loader = loader.getParent();
}
return jarPathMap;
}
}