View Javadoc

1   // ========================================================================
2   // Copyright (c) 1999-2009 Mort Bay Consulting Pty. Ltd.
3   // ------------------------------------------------------------------------
4   // All rights reserved. This program and the accompanying materials
5   // are made available under the terms of the Eclipse Public License v1.0
6   // and Apache License v2.0 which accompanies this distribution.
7   // The Eclipse Public License is available at 
8   // http://www.eclipse.org/legal/epl-v10.html
9   // The Apache License v2.0 is available at
10  // http://www.opensource.org/licenses/apache2.0.php
11  // You may elect to redistribute this code under either of these licenses. 
12  // ========================================================================
13  
14  package org.eclipse.jetty.webapp;
15  
16  import java.io.File;
17  import java.io.IOException;
18  import java.net.URL;
19  import java.net.URLClassLoader;
20  import java.security.CodeSource;
21  import java.security.PermissionCollection;
22  import java.util.ArrayList;
23  import java.util.Collections;
24  import java.util.Enumeration;
25  import java.util.HashSet;
26  import java.util.List;
27  import java.util.Set;
28  import java.util.StringTokenizer;
29  
30  import org.eclipse.jetty.util.StringUtil;
31  import org.eclipse.jetty.util.log.Log;
32  import org.eclipse.jetty.util.log.Logger;
33  import org.eclipse.jetty.util.resource.Resource;
34  import org.eclipse.jetty.util.resource.ResourceCollection;
35  
36  
37  /* ------------------------------------------------------------ */
38  /** ClassLoader for HttpContext.
39   * Specializes URLClassLoader with some utility and file mapping
40   * methods.
41   *
42   * This loader defaults to the 2.3 servlet spec behavior where non
43   * system classes are loaded from the classpath in preference to the
44   * parent loader.  Java2 compliant loading, where the parent loader
45   * always has priority, can be selected with the 
46   * {@link org.eclipse.jetty.webapp.WebAppContext#setParentLoaderPriority(boolean)} 
47   * method and influenced with {@link WebAppContext#isServerClass(String)} and 
48   * {@link WebAppContext#isSystemClass(String)}.
49   *
50   * If no parent class loader is provided, then the current thread 
51   * context classloader will be used.  If that is null then the 
52   * classloader that loaded this class is used as the parent.
53   * 
54   */
55  public class WebAppClassLoader extends URLClassLoader
56  {
57      private static final Logger LOG = Log.getLogger(WebAppClassLoader.class);
58  
59      private final Context _context;
60      private final ClassLoader _parent;
61      private final Set<String> _extensions=new HashSet<String>();
62      private String _name=String.valueOf(hashCode());
63      
64      /* ------------------------------------------------------------ */
65      /** The Context in which the classloader operates.
66       */
67      public interface Context
68      {
69          /* ------------------------------------------------------------ */
70          /** Convert a URL or path to a Resource.
71           * The default implementation
72           * is a wrapper for {@link Resource#newResource(String)}.
73           * @param urlOrPath The URL or path to convert
74           * @return The Resource for the URL/path
75           * @throws IOException The Resource could not be created.
76           */
77          Resource newResource(String urlOrPath) throws IOException;
78  
79          /* ------------------------------------------------------------ */
80          /**
81           * @return Returns the permissions.
82           */
83          PermissionCollection getPermissions();
84  
85          /* ------------------------------------------------------------ */
86          /** Is the class a System Class.
87           * A System class is a class that is visible to a webapplication,
88           * but that cannot be overridden by the contents of WEB-INF/lib or
89           * WEB-INF/classes 
90           * @param clazz The fully qualified name of the class.
91           * @return True if the class is a system class.
92           */
93          boolean isSystemClass(String clazz);
94  
95          /* ------------------------------------------------------------ */
96          /** Is the class a Server Class.
97           * A Server class is a class that is part of the implementation of 
98           * the server and is NIT visible to a webapplication. The web
99           * application may provide it's own implementation of the class,
100          * to be loaded from WEB-INF/lib or WEB-INF/classes 
101          * @param clazz The fully qualified name of the class.
102          * @return True if the class is a server class.
103          */
104         boolean isServerClass(String clazz);
105 
106         /* ------------------------------------------------------------ */
107         /**
108          * @return True if the classloader should delegate first to the parent 
109          * classloader (standard java behaviour) or false if the classloader 
110          * should first try to load from WEB-INF/lib or WEB-INF/classes (servlet 
111          * spec recommendation).
112          */
113         boolean isParentLoaderPriority();
114         
115         /* ------------------------------------------------------------ */
116         String getExtraClasspath();
117         
118     }
119     
120     /* ------------------------------------------------------------ */
121     /** Constructor.
122      */
123     public WebAppClassLoader(Context context)
124         throws IOException
125     {
126         this(null,context);
127     }
128     
129     /* ------------------------------------------------------------ */
130     /** Constructor.
131      */
132     public WebAppClassLoader(ClassLoader parent, Context context)
133         throws IOException
134     {
135         super(new URL[]{},parent!=null?parent
136                 :(Thread.currentThread().getContextClassLoader()!=null?Thread.currentThread().getContextClassLoader()
137                         :(WebAppClassLoader.class.getClassLoader()!=null?WebAppClassLoader.class.getClassLoader()
138                                 :ClassLoader.getSystemClassLoader())));
139         _parent=getParent();
140         _context=context;
141         if (_parent==null)
142             throw new IllegalArgumentException("no parent classloader!");
143         
144         _extensions.add(".jar");
145         _extensions.add(".zip");
146         
147         // TODO remove this system property
148         String extensions = System.getProperty(WebAppClassLoader.class.getName() + ".extensions");
149         if(extensions!=null)
150         {
151             StringTokenizer tokenizer = new StringTokenizer(extensions, ",;");
152             while(tokenizer.hasMoreTokens())
153                 _extensions.add(tokenizer.nextToken().trim());
154         }
155         
156         if (context.getExtraClasspath()!=null)
157             addClassPath(context.getExtraClasspath());
158     }
159     
160     /* ------------------------------------------------------------ */
161     /**
162      * @return the name of the classloader
163      */
164     public String getName()
165     {
166         return _name;
167     }
168 
169     /* ------------------------------------------------------------ */
170     /**
171      * @param name the name of the classloader
172      */
173     public void setName(String name)
174     {
175         _name=name;
176     }
177     
178 
179     /* ------------------------------------------------------------ */
180     public Context getContext()
181     {
182         return _context;
183     }
184 
185     /* ------------------------------------------------------------ */
186     /**
187      * @param resource Comma or semicolon separated path of filenames or URLs
188      * pointing to directories or jar files. Directories should end
189      * with '/'.
190      */
191     public void addClassPath(Resource resource)
192         throws IOException
193     {
194         if (resource instanceof ResourceCollection)
195         {
196             for (Resource r : ((ResourceCollection)resource).getResources())
197                 addClassPath(r);
198         }
199         else
200         {
201             addClassPath(resource.toString());
202         }
203     }
204     
205     /* ------------------------------------------------------------ */
206     /**
207      * @param classPath Comma or semicolon separated path of filenames or URLs
208      * pointing to directories or jar files. Directories should end
209      * with '/'.
210      */
211     public void addClassPath(String classPath)
212     	throws IOException
213     {
214         if (classPath == null)
215             return;
216             
217         StringTokenizer tokenizer= new StringTokenizer(classPath, ",;");
218         while (tokenizer.hasMoreTokens())
219         {
220             Resource resource= _context.newResource(tokenizer.nextToken().trim());
221             if (LOG.isDebugEnabled())
222                 LOG.debug("Path resource=" + resource);
223 
224             // Add the resource
225             if (resource.isDirectory() && resource instanceof ResourceCollection)
226                 addClassPath(resource);
227             else
228             {
229                 // Resolve file path if possible
230                 File file= resource.getFile();
231                 if (file != null)
232                 {
233                     URL url= resource.getURL();
234                     addURL(url);
235                 }
236                 else if (resource.isDirectory())
237                     addURL(resource.getURL());
238                 else
239                     throw new IllegalArgumentException("!file: "+resource);
240             }
241         }
242     }
243 
244     /* ------------------------------------------------------------ */
245     /**
246      * @param file Checks if this file type can be added to the classpath.
247      */
248     private boolean isFileSupported(String file)
249     {
250         int dot = file.lastIndexOf('.');
251         return dot!=-1 && _extensions.contains(file.substring(dot));
252     }
253     
254     /* ------------------------------------------------------------ */
255     /** Add elements to the class path for the context from the jar and zip files found
256      *  in the specified resource.
257      * @param lib the resource that contains the jar and/or zip files.
258      */
259     public void addJars(Resource lib)
260     {
261         if (lib.exists() && lib.isDirectory())
262         {
263             String[] files=lib.list();
264             for (int f=0;files!=null && f<files.length;f++)
265             {
266                 try 
267                 {
268                     Resource fn=lib.addPath(files[f]);
269                     String fnlc=fn.getName().toLowerCase();
270                     // don't check if this is a directory, see Bug 353165
271                     if (isFileSupported(fnlc))
272                     {
273                         String jar=fn.toString();
274                         jar=StringUtil.replace(jar, ",", "%2C");
275                         jar=StringUtil.replace(jar, ";", "%3B");
276                         addClassPath(jar);
277                     }
278                 }
279                 catch (Exception ex)
280                 {
281                     LOG.warn(Log.EXCEPTION,ex);
282                 }
283             }
284         }
285     }
286 
287     /* ------------------------------------------------------------ */
288     public PermissionCollection getPermissions(CodeSource cs)
289     {
290         // TODO check CodeSource
291         PermissionCollection permissions=_context.getPermissions();
292         PermissionCollection pc= (permissions == null) ? super.getPermissions(cs) : permissions;
293         return pc;
294     }
295 
296     /* ------------------------------------------------------------ */
297     public Enumeration<URL> getResources(String name) throws IOException
298     {
299         boolean system_class=_context.isSystemClass(name);
300         boolean server_class=_context.isServerClass(name);
301         
302         List<URL> from_parent = toList(server_class?null:_parent.getResources(name));
303         List<URL> from_webapp = toList((system_class&&!from_parent.isEmpty())?null:this.findResources(name));
304             
305         if (_context.isParentLoaderPriority())
306         {
307             from_parent.addAll(from_webapp);
308             return Collections.enumeration(from_parent);
309         }
310         from_webapp.addAll(from_parent);
311         return Collections.enumeration(from_webapp);
312     }
313 
314     /* ------------------------------------------------------------ */
315     private List<URL> toList(Enumeration<URL> e)
316     {
317         if (e==null)
318             return new ArrayList<URL>();
319         return Collections.list(e);
320     }
321     
322     /* ------------------------------------------------------------ */
323     /**
324      * Get a resource from the classloader
325      * 
326      * NOTE: this method provides a convenience of hacking off a leading /
327      * should one be present. This is non-standard and it is recommended 
328      * to not rely on this behavior
329      */
330     public URL getResource(String name)
331     {
332         URL url= null;
333         boolean tried_parent= false;
334         boolean system_class=_context.isSystemClass(name);
335         boolean server_class=_context.isServerClass(name);
336         
337         if (system_class && server_class)
338             return null;
339         
340         if (_parent!=null &&(_context.isParentLoaderPriority() || system_class ) && !server_class)
341         {
342             tried_parent= true;
343             
344             if (_parent!=null)
345                 url= _parent.getResource(name);
346         }
347 
348         if (url == null)
349         {
350             url= this.findResource(name);
351 
352             if (url == null && name.startsWith("/"))
353             {
354                 if (LOG.isDebugEnabled())
355                     LOG.debug("HACK leading / off " + name);
356                 url= this.findResource(name.substring(1));
357             }
358         }
359 
360         if (url == null && !tried_parent && !server_class )
361         {
362             if (_parent!=null)
363                 url= _parent.getResource(name);
364         }
365 
366         if (url != null)
367             if (LOG.isDebugEnabled())
368                 LOG.debug("getResource("+name+")=" + url);
369 
370         return url;
371     }
372 
373     /* ------------------------------------------------------------ */
374     @Override
375     public Class<?> loadClass(String name) throws ClassNotFoundException
376     {
377         return loadClass(name, false);
378     }
379 
380     /* ------------------------------------------------------------ */
381     @Override
382     protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException
383     {
384         Class<?> c= findLoadedClass(name);
385         ClassNotFoundException ex= null;
386         boolean tried_parent= false;
387         
388         boolean system_class=_context.isSystemClass(name);
389         boolean server_class=_context.isServerClass(name);
390         
391         if (system_class && server_class)
392         {
393             return null;
394         }
395         
396         if (c == null && _parent!=null && (_context.isParentLoaderPriority() || system_class) && !server_class)
397         {
398             tried_parent= true;
399             try
400             {
401                 c= _parent.loadClass(name);
402                 if (LOG.isDebugEnabled())
403                     LOG.debug("loaded " + c);
404             }
405             catch (ClassNotFoundException e)
406             {
407                 ex= e;
408             }
409         }
410 
411         if (c == null)
412         {
413             try
414             {
415                 c= this.findClass(name);
416             }
417             catch (ClassNotFoundException e)
418             {
419                 ex= e;
420             }
421         }
422 
423         if (c == null && _parent!=null && !tried_parent && !server_class )
424             c= _parent.loadClass(name);
425 
426         if (c == null)
427             throw ex;
428 
429         if (resolve)
430             resolveClass(c);
431 
432         if (LOG.isDebugEnabled())
433             LOG.debug("loaded " + c+ " from "+c.getClassLoader());
434         
435         return c;
436     }
437 
438     /* ------------------------------------------------------------ */
439     public String toString()
440     {
441         return "WebAppClassLoader=" + _name+"@"+Long.toHexString(hashCode());
442     }
443 }