View Javadoc

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