View Javadoc

1   //
2   //  ========================================================================
3   //  Copyright (c) 1995-2014 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.io.InputStream;
24  import java.lang.instrument.ClassFileTransformer;
25  import java.lang.instrument.IllegalClassFormatException;
26  import java.lang.instrument.Instrumentation;
27  import java.net.URL;
28  import java.net.URLClassLoader;
29  import java.security.CodeSource;
30  import java.security.PermissionCollection;
31  import java.util.ArrayList;
32  import java.util.Collections;
33  import java.util.Enumeration;
34  import java.util.HashSet;
35  import java.util.List;
36  import java.util.Locale;
37  import java.util.Set;
38  import java.util.StringTokenizer;
39  import java.util.concurrent.CopyOnWriteArrayList;
40  
41  import org.eclipse.jetty.util.IO;
42  import org.eclipse.jetty.util.StringUtil;
43  import org.eclipse.jetty.util.log.Log;
44  import org.eclipse.jetty.util.log.Logger;
45  import org.eclipse.jetty.util.resource.Resource;
46  import org.eclipse.jetty.util.resource.ResourceCollection;
47  
48  
49  /* ------------------------------------------------------------ */
50  /** ClassLoader for HttpContext.
51   * Specializes URLClassLoader with some utility and file mapping
52   * methods.
53   *
54   * This loader defaults to the 2.3 servlet spec behavior where non
55   * system classes are loaded from the classpath in preference to the
56   * parent loader.  Java2 compliant loading, where the parent loader
57   * always has priority, can be selected with the 
58   * {@link org.eclipse.jetty.webapp.WebAppContext#setParentLoaderPriority(boolean)} 
59   * method and influenced with {@link WebAppContext#isServerClass(String)} and 
60   * {@link WebAppContext#isSystemClass(String)}.
61   *
62   * If no parent class loader is provided, then the current thread 
63   * context classloader will be used.  If that is null then the 
64   * classloader that loaded this class is used as the parent.
65   * 
66   */
67  public class WebAppClassLoader extends URLClassLoader
68  {
69      private static final Logger LOG = Log.getLogger(WebAppClassLoader.class);
70  
71      private final Context _context;
72      private final ClassLoader _parent;
73      private final Set<String> _extensions=new HashSet<String>();
74      private String _name=String.valueOf(hashCode());
75      private final List<ClassFileTransformer> _transformers = new CopyOnWriteArrayList<>();
76      
77      /* ------------------------------------------------------------ */
78      /** The Context in which the classloader operates.
79       */
80      public interface Context
81      {
82          /* ------------------------------------------------------------ */
83          /** Convert a URL or path to a Resource.
84           * The default implementation
85           * is a wrapper for {@link Resource#newResource(String)}.
86           * @param urlOrPath The URL or path to convert
87           * @return The Resource for the URL/path
88           * @throws IOException The Resource could not be created.
89           */
90          Resource newResource(String urlOrPath) throws IOException;
91  
92          /* ------------------------------------------------------------ */
93          /**
94           * @return Returns the permissions.
95           */
96          PermissionCollection getPermissions();
97  
98          /* ------------------------------------------------------------ */
99          /** Is the class a System Class.
100          * A System class is a class that is visible to a webapplication,
101          * but that cannot be overridden by the contents of WEB-INF/lib or
102          * WEB-INF/classes 
103          * @param clazz The fully qualified name of the class.
104          * @return True if the class is a system class.
105          */
106         boolean isSystemClass(String clazz);
107 
108         /* ------------------------------------------------------------ */
109         /** Is the class a Server Class.
110          * A Server class is a class that is part of the implementation of 
111          * the server and is NIT visible to a webapplication. The web
112          * application may provide it's own implementation of the class,
113          * to be loaded from WEB-INF/lib or WEB-INF/classes 
114          * @param clazz The fully qualified name of the class.
115          * @return True if the class is a server class.
116          */
117         boolean isServerClass(String clazz);
118 
119         /* ------------------------------------------------------------ */
120         /**
121          * @return True if the classloader should delegate first to the parent 
122          * classloader (standard java behaviour) or false if the classloader 
123          * should first try to load from WEB-INF/lib or WEB-INF/classes (servlet 
124          * spec recommendation).
125          */
126         boolean isParentLoaderPriority();
127         
128         /* ------------------------------------------------------------ */
129         String getExtraClasspath();
130         
131     }
132     
133     /* ------------------------------------------------------------ */
134     /** Constructor.
135      */
136     public WebAppClassLoader(Context context)
137         throws IOException
138     {
139         this(null,context);
140     }
141     
142     /* ------------------------------------------------------------ */
143     /** Constructor.
144      */
145     public WebAppClassLoader(ClassLoader parent, Context context)
146         throws IOException
147     {
148         super(new URL[]{},parent!=null?parent
149                 :(Thread.currentThread().getContextClassLoader()!=null?Thread.currentThread().getContextClassLoader()
150                         :(WebAppClassLoader.class.getClassLoader()!=null?WebAppClassLoader.class.getClassLoader()
151                                 :ClassLoader.getSystemClassLoader())));
152         _parent=getParent();
153         _context=context;
154         if (_parent==null)
155             throw new IllegalArgumentException("no parent classloader!");
156         
157         _extensions.add(".jar");
158         _extensions.add(".zip");
159         
160         // TODO remove this system property
161         String extensions = System.getProperty(WebAppClassLoader.class.getName() + ".extensions");
162         if(extensions!=null)
163         {
164             StringTokenizer tokenizer = new StringTokenizer(extensions, ",;");
165             while(tokenizer.hasMoreTokens())
166                 _extensions.add(tokenizer.nextToken().trim());
167         }
168         
169         if (context.getExtraClasspath()!=null)
170             addClassPath(context.getExtraClasspath());
171     }
172     
173     /* ------------------------------------------------------------ */
174     /**
175      * @return the name of the classloader
176      */
177     public String getName()
178     {
179         return _name;
180     }
181 
182     /* ------------------------------------------------------------ */
183     /**
184      * @param name the name of the classloader
185      */
186     public void setName(String name)
187     {
188         _name=name;
189     }
190     
191 
192     /* ------------------------------------------------------------ */
193     public Context getContext()
194     {
195         return _context;
196     }
197 
198     /* ------------------------------------------------------------ */
199     /**
200      * @param resource Comma or semicolon separated path of filenames or URLs
201      * pointing to directories or jar files. Directories should end
202      * with '/'.
203      */
204     public void addClassPath(Resource resource)
205         throws IOException
206     {
207         if (resource instanceof ResourceCollection)
208         {
209             for (Resource r : ((ResourceCollection)resource).getResources())
210                 addClassPath(r);
211         }
212         else
213         {
214             addClassPath(resource.toString());
215         }
216     }
217     
218     /* ------------------------------------------------------------ */
219     /**
220      * @param classPath Comma or semicolon separated path of filenames or URLs
221      * pointing to directories or jar files. Directories should end
222      * with '/'.
223      */
224     public void addClassPath(String classPath)
225         throws IOException
226     {
227         if (classPath == null)
228             return;
229             
230         StringTokenizer tokenizer= new StringTokenizer(classPath, ",;");
231         while (tokenizer.hasMoreTokens())
232         {
233             Resource resource= _context.newResource(tokenizer.nextToken().trim());
234             if (LOG.isDebugEnabled())
235                 LOG.debug("Path resource=" + resource);
236 
237             // Add the resource
238             if (resource.isDirectory() && resource instanceof ResourceCollection)
239                 addClassPath(resource);
240             else
241             {
242                 // Resolve file path if possible
243                 File file= resource.getFile();
244                 if (file != null)
245                 {
246                     URL url= resource.getURL();
247                     addURL(url);
248                 }
249                 else if (resource.isDirectory())
250                     addURL(resource.getURL());
251                 else
252                 {
253                     if (LOG.isDebugEnabled())
254                         LOG.debug("Check file exists and is not nested jar: "+resource);
255                     throw new IllegalArgumentException("File not resolvable or incompatible with URLClassloader: "+resource);
256                 }
257             }
258         }
259     }
260 
261     /* ------------------------------------------------------------ */
262     /**
263      * @param file Checks if this file type can be added to the classpath.
264      */
265     private boolean isFileSupported(String file)
266     {
267         int dot = file.lastIndexOf('.');
268         return dot!=-1 && _extensions.contains(file.substring(dot));
269     }
270     
271     /* ------------------------------------------------------------ */
272     /** Add elements to the class path for the context from the jar and zip files found
273      *  in the specified resource.
274      * @param lib the resource that contains the jar and/or zip files.
275      */
276     public void addJars(Resource lib)
277     {
278         if (lib.exists() && lib.isDirectory())
279         {
280             String[] files=lib.list();
281             for (int f=0;files!=null && f<files.length;f++)
282             {
283                 try 
284                 {
285                     Resource fn=lib.addPath(files[f]);
286                     String fnlc=fn.getName().toLowerCase(Locale.ENGLISH);
287                     // don't check if this is a directory, see Bug 353165
288                     if (isFileSupported(fnlc))
289                     {
290                         String jar=fn.toString();
291                         jar=StringUtil.replace(jar, ",", "%2C");
292                         jar=StringUtil.replace(jar, ";", "%3B");
293                         addClassPath(jar);
294                     }
295                 }
296                 catch (Exception ex)
297                 {
298                     LOG.warn(Log.EXCEPTION,ex);
299                 }
300             }
301         }
302     }
303 
304     /* ------------------------------------------------------------ */
305     @Override
306     public PermissionCollection getPermissions(CodeSource cs)
307     {
308         PermissionCollection permissions=_context.getPermissions();
309         PermissionCollection pc= (permissions == null) ? super.getPermissions(cs) : permissions;
310         return pc;
311     }
312 
313     /* ------------------------------------------------------------ */
314     @Override
315     public Enumeration<URL> getResources(String name) throws IOException
316     {
317         boolean system_class=_context.isSystemClass(name);
318         boolean server_class=_context.isServerClass(name);
319         
320         List<URL> from_parent = toList(server_class?null:_parent.getResources(name));
321         List<URL> from_webapp = toList((system_class&&!from_parent.isEmpty())?null:this.findResources(name));
322             
323         if (_context.isParentLoaderPriority())
324         {
325             from_parent.addAll(from_webapp);
326             return Collections.enumeration(from_parent);
327         }
328         from_webapp.addAll(from_parent);
329         return Collections.enumeration(from_webapp);
330     }
331 
332     /* ------------------------------------------------------------ */
333     private List<URL> toList(Enumeration<URL> e)
334     {
335         if (e==null)
336             return new ArrayList<URL>();
337         return Collections.list(e);
338     }
339     
340     /* ------------------------------------------------------------ */
341     /**
342      * Get a resource from the classloader
343      * 
344      * NOTE: this method provides a convenience of hacking off a leading /
345      * should one be present. This is non-standard and it is recommended 
346      * to not rely on this behavior
347      */
348     @Override
349     public URL getResource(String name)
350     {
351         URL url= null;
352         boolean tried_parent= false;
353 
354         //If the resource is a class name with .class suffix, strip it off before comparison
355         //as the server and system patterns are specified without a .class suffix
356         String tmp = name;
357         if (tmp != null && tmp.endsWith(".class"))
358             tmp = tmp.substring(0, tmp.length()-6);
359       
360         boolean system_class=_context.isSystemClass(tmp);
361         boolean server_class=_context.isServerClass(tmp);
362         
363         if (system_class && server_class)
364             return null;
365         
366         if (_parent!=null &&(_context.isParentLoaderPriority() || system_class ) && !server_class)
367         {
368             tried_parent= true;
369             
370             if (_parent!=null)
371                 url= _parent.getResource(name);
372         }
373 
374         if (url == null)
375         {
376             url= this.findResource(name);
377 
378             if (url == null && name.startsWith("/"))
379             {
380                 if (LOG.isDebugEnabled())
381                     LOG.debug("HACK leading / off " + name);
382                 url= this.findResource(name.substring(1));
383             }
384         }
385 
386         if (url == null && !tried_parent && !server_class )
387         {
388             if (_parent!=null)
389                 url= _parent.getResource(name);
390         }
391 
392         if (url != null)
393             if (LOG.isDebugEnabled())
394                 LOG.debug("getResource("+name+")=" + url);
395 
396         return url;
397     }
398 
399     /* ------------------------------------------------------------ */
400     @Override
401     public Class<?> loadClass(String name) throws ClassNotFoundException
402     {
403         return loadClass(name, false);
404     }
405 
406     /* ------------------------------------------------------------ */
407     @Override
408     protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException
409     {
410         Class<?> c= findLoadedClass(name);
411         ClassNotFoundException ex= null;
412         boolean tried_parent= false;
413         
414         boolean system_class=_context.isSystemClass(name);
415         boolean server_class=_context.isServerClass(name);
416         
417         if (system_class && server_class)
418         {
419             return null;
420         }
421         
422         if (c == null && _parent!=null && (_context.isParentLoaderPriority() || system_class) && !server_class)
423         {
424             tried_parent= true;
425             try
426             {
427                 c= _parent.loadClass(name);
428                 if (LOG.isDebugEnabled())
429                     LOG.debug("loaded " + c);
430             }
431             catch (ClassNotFoundException e)
432             {
433                 ex= e;
434             }
435         }
436 
437         if (c == null)
438         {
439             try
440             {
441                 c= this.findClass(name);
442             }
443             catch (ClassNotFoundException e)
444             {
445                 ex= e;
446             }
447         }
448 
449         if (c == null && _parent!=null && !tried_parent && !server_class )
450             c= _parent.loadClass(name);
451 
452         if (c == null && ex!=null)
453             throw ex;
454 
455         if (resolve)
456             resolveClass(c);
457 
458         if (LOG.isDebugEnabled())
459             LOG.debug("loaded {} from {}",c,c==null?null:c.getClassLoader());
460         
461         return c;
462     }
463 
464     /* ------------------------------------------------------------ */
465     /**
466      * @see addTransformer
467      * @deprecated
468      */
469     public void addClassFileTransformer(ClassFileTransformer transformer)
470     {
471         _transformers.add(transformer);
472     }
473     
474     /* ------------------------------------------------------------ */
475     /**
476      * @see removeTransformer
477      * @deprecated
478      */
479     public boolean removeClassFileTransformer(ClassFileTransformer transformer)
480     {
481         return _transformers.remove(transformer);
482     }
483 
484     /* ------------------------------------------------------------ */
485     /**
486      * @see addClassFileTransformer
487      */
488     public void addTransformer(ClassFileTransformer transformer)
489     {
490         _transformers.add(transformer);
491     }
492     
493     /* ------------------------------------------------------------ */
494     /**
495      * @see removeClassFileTransformer
496      */
497     public boolean removeTransformer(ClassFileTransformer transformer)
498     {
499         return _transformers.remove(transformer);
500     }
501     
502     
503     /* ------------------------------------------------------------ */
504     @Override
505     protected Class<?> findClass(final String name) throws ClassNotFoundException
506     {
507         Class<?> clazz=null;
508 
509         if (_transformers.isEmpty())
510             clazz = super.findClass(name);
511         else
512         {
513             String path = name.replace('.', '/').concat(".class");
514             URL url = getResource(path);
515             if (url==null)
516                 throw new ClassNotFoundException(name);
517 
518             InputStream content=null;
519             try
520             {
521                 content = url.openStream();
522                 byte[] bytes =IO.readBytes(content);
523                     
524                 for (ClassFileTransformer transformer : _transformers)
525                     bytes=transformer.transform(this,name,null,null,bytes);
526                 
527                 clazz=defineClass(name,bytes,0,bytes.length);
528             }
529             catch (IOException e)
530             {
531                 throw new ClassNotFoundException(name,e);
532             }
533             catch (IllegalClassFormatException e)
534             {
535                 throw new ClassNotFoundException(name,e);
536             }
537             finally
538             {
539                 if (content!=null)
540                 {
541                     try
542                     {
543                         content.close(); 
544                     }
545                     catch (IOException e)
546                     {
547                         throw new ClassNotFoundException(name,e);
548                     }
549                 }
550             }
551         }
552 
553         return clazz;
554     }
555     
556     /* ------------------------------------------------------------ */
557     @Override
558     public String toString()
559     {
560         return "WebAppClassLoader=" + _name+"@"+Long.toHexString(hashCode());
561     }
562 }