// ======================================================================== // Copyright (c) 1999-2009 Mort Bay Consulting Pty. Ltd. // ------------------------------------------------------------------------ // All rights reserved. This program and the accompanying materials // are made available under the terms of the Eclipse Public License v1.0 // and Apache License v2.0 which accompanies this distribution. // The Eclipse Public License is available at // http://www.eclipse.org/legal/epl-v10.html // The Apache License v2.0 is available at // http://www.opensource.org/licenses/apache2.0.php // You may elect to redistribute this code under either of these licenses. // ======================================================================== package org.eclipse.jetty.webapp; import java.io.File; import java.io.IOException; import java.net.URL; import java.net.URLClassLoader; import java.security.CodeSource; import java.security.PermissionCollection; import java.util.ArrayList; import java.util.Collections; import java.util.Enumeration; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.StringTokenizer; import org.eclipse.jetty.util.StringUtil; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; import org.eclipse.jetty.util.resource.Resource; import org.eclipse.jetty.util.resource.ResourceCollection; /* ------------------------------------------------------------ */ /** ClassLoader for HttpContext. * Specializes URLClassLoader with some utility and file mapping * methods. * * This loader defaults to the 2.3 servlet spec behavior where non * system classes are loaded from the classpath in preference to the * parent loader. Java2 compliant loading, where the parent loader * always has priority, can be selected with the * {@link org.eclipse.jetty.webapp.WebAppContext#setParentLoaderPriority(boolean)} * method and influenced with {@link WebAppContext#isServerClass(String)} and * {@link WebAppContext#isSystemClass(String)}. * * If no parent class loader is provided, then the current thread * context classloader will be used. If that is null then the * classloader that loaded this class is used as the parent. * */ public class WebAppClassLoader extends URLClassLoader { private static final Logger LOG = Log.getLogger(WebAppClassLoader.class); private final Context _context; private final ClassLoader _parent; private final Set<String> _extensions=new HashSet<String>(); private String _name=String.valueOf(hashCode()); /* ------------------------------------------------------------ */ /** The Context in which the classloader operates. */ public interface Context { /* ------------------------------------------------------------ */ /** Convert a URL or path to a Resource. * The default implementation * is a wrapper for {@link Resource#newResource(String)}. * @param urlOrPath The URL or path to convert * @return The Resource for the URL/path * @throws IOException The Resource could not be created. */ Resource newResource(String urlOrPath) throws IOException; /* ------------------------------------------------------------ */ /** * @return Returns the permissions. */ PermissionCollection getPermissions(); /* ------------------------------------------------------------ */ /** Is the class a System Class. * A System class is a class that is visible to a webapplication, * but that cannot be overridden by the contents of WEB-INF/lib or * WEB-INF/classes * @param clazz The fully qualified name of the class. * @return True if the class is a system class. */ boolean isSystemClass(String clazz); /* ------------------------------------------------------------ */ /** Is the class a Server Class. * A Server class is a class that is part of the implementation of * the server and is NIT visible to a webapplication. The web * application may provide it's own implementation of the class, * to be loaded from WEB-INF/lib or WEB-INF/classes * @param clazz The fully qualified name of the class. * @return True if the class is a server class. */ boolean isServerClass(String clazz); /* ------------------------------------------------------------ */ /** * @return True if the classloader should delegate first to the parent * classloader (standard java behaviour) or false if the classloader * should first try to load from WEB-INF/lib or WEB-INF/classes (servlet * spec recommendation). */ boolean isParentLoaderPriority(); /* ------------------------------------------------------------ */ String getExtraClasspath(); } /* ------------------------------------------------------------ */ /** Constructor. */ public WebAppClassLoader(Context context) throws IOException { this(null,context); } /* ------------------------------------------------------------ */ /** Constructor. */ public WebAppClassLoader(ClassLoader parent, Context context) throws IOException { super(new URL[]{},parent!=null?parent :(Thread.currentThread().getContextClassLoader()!=null?Thread.currentThread().getContextClassLoader() :(WebAppClassLoader.class.getClassLoader()!=null?WebAppClassLoader.class.getClassLoader() :ClassLoader.getSystemClassLoader()))); _parent=getParent(); _context=context; if (_parent==null) throw new IllegalArgumentException("no parent classloader!"); _extensions.add(".jar"); _extensions.add(".zip"); // TODO remove this system property String extensions = System.getProperty(WebAppClassLoader.class.getName() + ".extensions"); if(extensions!=null) { StringTokenizer tokenizer = new StringTokenizer(extensions, ",;"); while(tokenizer.hasMoreTokens()) _extensions.add(tokenizer.nextToken().trim()); } if (context.getExtraClasspath()!=null) addClassPath(context.getExtraClasspath()); } /* ------------------------------------------------------------ */ /** * @return the name of the classloader */ public String getName() { return _name; } /* ------------------------------------------------------------ */ /** * @param name the name of the classloader */ public void setName(String name) { _name=name; } /* ------------------------------------------------------------ */ public Context getContext() { return _context; } /* ------------------------------------------------------------ */ /** * @param resource Comma or semicolon separated path of filenames or URLs * pointing to directories or jar files. Directories should end * with '/'. */ public void addClassPath(Resource resource) throws IOException { if (resource instanceof ResourceCollection) { for (Resource r : ((ResourceCollection)resource).getResources()) addClassPath(r); } else { addClassPath(resource.toString()); } } /* ------------------------------------------------------------ */ /** * @param classPath Comma or semicolon separated path of filenames or URLs * pointing to directories or jar files. Directories should end * with '/'. */ public void addClassPath(String classPath) throws IOException { if (classPath == null) return; StringTokenizer tokenizer= new StringTokenizer(classPath, ",;"); while (tokenizer.hasMoreTokens()) { Resource resource= _context.newResource(tokenizer.nextToken().trim()); if (LOG.isDebugEnabled()) LOG.debug("Path resource=" + resource); // Add the resource if (resource.isDirectory() && resource instanceof ResourceCollection) addClassPath(resource); else { // Resolve file path if possible File file= resource.getFile(); if (file != null) { URL url= resource.getURL(); addURL(url); } else if (resource.isDirectory()) addURL(resource.getURL()); else throw new IllegalArgumentException("!file: "+resource); } } } /* ------------------------------------------------------------ */ /** * @param file Checks if this file type can be added to the classpath. */ private boolean isFileSupported(String file) { int dot = file.lastIndexOf('.'); return dot!=-1 && _extensions.contains(file.substring(dot)); } /* ------------------------------------------------------------ */ /** Add elements to the class path for the context from the jar and zip files found * in the specified resource. * @param lib the resource that contains the jar and/or zip files. */ public void addJars(Resource lib) { if (lib.exists() && lib.isDirectory()) { String[] files=lib.list(); for (int f=0;files!=null && f<files.length;f++) { try { Resource fn=lib.addPath(files[f]); String fnlc=fn.getName().toLowerCase(); // don't check if this is a directory, see Bug 353165 if (isFileSupported(fnlc)) { String jar=fn.toString(); jar=StringUtil.replace(jar, ",", "%2C"); jar=StringUtil.replace(jar, ";", "%3B"); addClassPath(jar); } } catch (Exception ex) { LOG.warn(Log.EXCEPTION,ex); } } } } /* ------------------------------------------------------------ */ public PermissionCollection getPermissions(CodeSource cs) { // TODO check CodeSource PermissionCollection permissions=_context.getPermissions(); PermissionCollection pc= (permissions == null) ? super.getPermissions(cs) : permissions; return pc; } /* ------------------------------------------------------------ */ public Enumeration<URL> getResources(String name) throws IOException { boolean system_class=_context.isSystemClass(name); boolean server_class=_context.isServerClass(name); List<URL> from_parent = toList(server_class?null:_parent.getResources(name)); List<URL> from_webapp = toList((system_class&&!from_parent.isEmpty())?null:this.findResources(name)); if (_context.isParentLoaderPriority()) { from_parent.addAll(from_webapp); return Collections.enumeration(from_parent); } from_webapp.addAll(from_parent); return Collections.enumeration(from_webapp); } /* ------------------------------------------------------------ */ private List<URL> toList(Enumeration<URL> e) { if (e==null) return new ArrayList<URL>(); return Collections.list(e); } /* ------------------------------------------------------------ */ /** * Get a resource from the classloader * * NOTE: this method provides a convenience of hacking off a leading / * should one be present. This is non-standard and it is recommended * to not rely on this behavior */ public URL getResource(String name) { URL url= null; boolean tried_parent= false; boolean system_class=_context.isSystemClass(name); boolean server_class=_context.isServerClass(name); if (system_class && server_class) return null; if (_parent!=null &&(_context.isParentLoaderPriority() || system_class ) && !server_class) { tried_parent= true; if (_parent!=null) url= _parent.getResource(name); } if (url == null) { url= this.findResource(name); if (url == null && name.startsWith("/")) { if (LOG.isDebugEnabled()) LOG.debug("HACK leading / off " + name); url= this.findResource(name.substring(1)); } } if (url == null && !tried_parent && !server_class ) { if (_parent!=null) url= _parent.getResource(name); } if (url != null) if (LOG.isDebugEnabled()) LOG.debug("getResource("+name+")=" + url); return url; } /* ------------------------------------------------------------ */ @Override public Class<?> loadClass(String name) throws ClassNotFoundException { return loadClass(name, false); } /* ------------------------------------------------------------ */ @Override protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { Class<?> c= findLoadedClass(name); ClassNotFoundException ex= null; boolean tried_parent= false; boolean system_class=_context.isSystemClass(name); boolean server_class=_context.isServerClass(name); if (system_class && server_class) { return null; } if (c == null && _parent!=null && (_context.isParentLoaderPriority() || system_class) && !server_class) { tried_parent= true; try { c= _parent.loadClass(name); if (LOG.isDebugEnabled()) LOG.debug("loaded " + c); } catch (ClassNotFoundException e) { ex= e; } } if (c == null) { try { c= this.findClass(name); } catch (ClassNotFoundException e) { ex= e; } } if (c == null && _parent!=null && !tried_parent && !server_class ) c= _parent.loadClass(name); if (c == null) throw ex; if (resolve) resolveClass(c); if (LOG.isDebugEnabled()) LOG.debug("loaded " + c+ " from "+c.getClassLoader()); return c; } /* ------------------------------------------------------------ */ public String toString() { return "WebAppClassLoader=" + _name+"@"+Long.toHexString(hashCode()); } }