View Javadoc

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