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.security.PrivilegedExceptionAction;
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   * <p>
52   * Specializes URLClassLoader with some utility and file mapping
53   * methods.
54   * <p>
55   * This loader defaults to the 2.3 servlet spec behavior where non
56   * system classes are loaded from the classpath in preference to the
57   * parent loader.  Java2 compliant loading, where the parent loader
58   * always has priority, can be selected with the 
59   * {@link org.eclipse.jetty.webapp.WebAppContext#setParentLoaderPriority(boolean)} 
60   * method and influenced with {@link WebAppContext#isServerClass(String)} and 
61   * {@link WebAppContext#isSystemClass(String)}.
62   * <p>
63   * If no parent class loader is provided, then the current thread 
64   * context classloader will be used.  If that is null then the 
65   * classloader that loaded this class is used as the parent.
66   */
67  public class WebAppClassLoader extends URLClassLoader
68  {
69      static
70      {
71          registerAsParallelCapable();
72      }
73  
74      private static final Logger LOG = Log.getLogger(WebAppClassLoader.class);
75      private static final ThreadLocal<Boolean> __loadServerClasses = new ThreadLocal<>();
76      
77      private final Context _context;
78      private final ClassLoader _parent;
79      private final Set<String> _extensions=new HashSet<String>();
80      private String _name=String.valueOf(hashCode());
81      private final List<ClassFileTransformer> _transformers = new CopyOnWriteArrayList<>();
82      
83      
84      /* ------------------------------------------------------------ */
85      /** The Context in which the classloader operates.
86       */
87      public interface Context
88      {
89          /* ------------------------------------------------------------ */
90          /** Convert a URL or path to a Resource.
91           * The default implementation
92           * is a wrapper for {@link Resource#newResource(String)}.
93           * @param urlOrPath The URL or path to convert
94           * @return The Resource for the URL/path
95           * @throws IOException The Resource could not be created.
96           */
97          Resource newResource(String urlOrPath) throws IOException;
98  
99          /* ------------------------------------------------------------ */
100         /**
101          * @return Returns the permissions.
102          */
103         PermissionCollection getPermissions();
104         
105         /* ------------------------------------------------------------ */
106         /** Is the class a System Class.
107          * A System class is a class that is visible to a webapplication,
108          * but that cannot be overridden by the contents of WEB-INF/lib or
109          * WEB-INF/classes 
110          * @param clazz The fully qualified name of the class.
111          * @return True if the class is a system class.
112          */
113         boolean isSystemClass(Class<?> clazz);
114 
115         /* ------------------------------------------------------------ */
116         /** Is the class a Server Class.
117          * A Server class is a class that is part of the implementation of 
118          * the server and is NIT visible to a webapplication. The web
119          * application may provide it's own implementation of the class,
120          * to be loaded from WEB-INF/lib or WEB-INF/classes 
121          * @param clazz The fully qualified name of the class.
122          * @return True if the class is a server class.
123          */
124         boolean isServerClass(Class<?> clazz);
125 
126         /* ------------------------------------------------------------ */
127         /**
128          * @return True if the classloader should delegate first to the parent 
129          * classloader (standard java behaviour) or false if the classloader 
130          * should first try to load from WEB-INF/lib or WEB-INF/classes (servlet 
131          * spec recommendation).
132          */
133         boolean isParentLoaderPriority();
134         
135         /* ------------------------------------------------------------ */
136         String getExtraClasspath();
137 
138         boolean isServerResource(String name, URL parent_url);
139 
140         boolean isSystemResource(String name, URL webapp_url);
141         
142     }
143 
144     /* ------------------------------------------------------------ */
145     /** Run an action with access to ServerClasses
146      * <p>Run the passed {@link PrivilegedExceptionAction} with the classloader
147      * configured so as to allow server classes to be visible</p>
148      * @param <T> The type returned by the action
149      * @param action The action to run
150      * @return The return from the action
151      * @throws Exception if thrown by the action
152      */
153     public static <T> T runWithServerClassAccess(PrivilegedExceptionAction<T> action) throws Exception
154     {
155         Boolean lsc=__loadServerClasses.get();
156         try
157         {
158             __loadServerClasses.set(true);
159             return action.run();
160         }
161         finally
162         {
163             if (lsc==null)
164                 __loadServerClasses.remove();
165             else
166                 __loadServerClasses.set(lsc);
167         }
168     }
169     
170     /* ------------------------------------------------------------ */
171     /** 
172      * Constructor.
173      * @param context the context for this classloader
174      * @throws IOException if unable to initialize from context
175      */
176     public WebAppClassLoader(Context context)
177         throws IOException
178     {
179         this(null,context);
180     }
181     
182     /* ------------------------------------------------------------ */
183     /** 
184      * Constructor.
185      * 
186      * @param parent the parent classloader 
187      * @param context the context for this classloader
188      * @throws IOException if unable to initialize classloader
189      */
190     public WebAppClassLoader(ClassLoader parent, Context context)
191         throws IOException
192     {
193         super(new URL[]{},parent!=null?parent
194                 :(Thread.currentThread().getContextClassLoader()!=null?Thread.currentThread().getContextClassLoader()
195                         :(WebAppClassLoader.class.getClassLoader()!=null?WebAppClassLoader.class.getClassLoader()
196                                 :ClassLoader.getSystemClassLoader())));
197         _parent=getParent();
198         _context=context;
199         if (_parent==null)
200             throw new IllegalArgumentException("no parent classloader!");
201         
202         _extensions.add(".jar");
203         _extensions.add(".zip");
204         
205         // TODO remove this system property
206         String extensions = System.getProperty(WebAppClassLoader.class.getName() + ".extensions");
207         if(extensions!=null)
208         {
209             StringTokenizer tokenizer = new StringTokenizer(extensions, ",;");
210             while(tokenizer.hasMoreTokens())
211                 _extensions.add(tokenizer.nextToken().trim());
212         }
213         
214         if (context.getExtraClasspath()!=null)
215             addClassPath(context.getExtraClasspath());
216     }
217     
218     /* ------------------------------------------------------------ */
219     /**
220      * @return the name of the classloader
221      */
222     public String getName()
223     {
224         return _name;
225     }
226 
227     /* ------------------------------------------------------------ */
228     /**
229      * @param name the name of the classloader
230      */
231     public void setName(String name)
232     {
233         _name=name;
234     }
235     
236 
237     /* ------------------------------------------------------------ */
238     public Context getContext()
239     {
240         return _context;
241     }
242 
243     /* ------------------------------------------------------------ */
244     /**
245      * @param resource Comma or semicolon separated path of filenames or URLs
246      * pointing to directories or jar files. Directories should end
247      * with '/'.
248      * @throws IOException if unable to add classpath from resource
249      */
250     public void addClassPath(Resource resource)
251         throws IOException
252     {
253         if (resource instanceof ResourceCollection)
254         {
255             for (Resource r : ((ResourceCollection)resource).getResources())
256                 addClassPath(r);
257         }
258         else
259         {
260             addClassPath(resource.toString());
261         }
262     }
263     
264     /* ------------------------------------------------------------ */
265     /**
266      * @param classPath Comma or semicolon separated path of filenames or URLs
267      * pointing to directories or jar files. Directories should end
268      * with '/'.
269      * @throws IOException if unable to add classpath
270      */
271     public void addClassPath(String classPath)
272         throws IOException
273     {
274         if (classPath == null)
275             return;
276             
277         StringTokenizer tokenizer= new StringTokenizer(classPath, ",;");
278         while (tokenizer.hasMoreTokens())
279         {
280             Resource resource= _context.newResource(tokenizer.nextToken().trim());
281             if (LOG.isDebugEnabled())
282                 LOG.debug("Path resource=" + resource);
283 
284             // Add the resource
285             if (resource.isDirectory() && resource instanceof ResourceCollection)
286                 addClassPath(resource);
287             else
288             {
289                 // Resolve file path if possible
290                 File file= resource.getFile();
291                 if (file != null)
292                 {
293                     URL url= resource.getURI().toURL();
294                     addURL(url);
295                 }
296                 else if (resource.isDirectory())
297                 {
298                     addURL(resource.getURI().toURL());
299                 }
300                 else
301                 {
302                     if (LOG.isDebugEnabled())
303                         LOG.debug("Check file exists and is not nested jar: "+resource);
304                     throw new IllegalArgumentException("File not resolvable or incompatible with URLClassloader: "+resource);
305                 }
306             }
307         }
308     }
309 
310     /* ------------------------------------------------------------ */
311     /**
312      * @param file Checks if this file type can be added to the classpath.
313      */
314     private boolean isFileSupported(String file)
315     {
316         int dot = file.lastIndexOf('.');
317         return dot!=-1 && _extensions.contains(file.substring(dot));
318     }
319     
320     /* ------------------------------------------------------------ */
321     /** Add elements to the class path for the context from the jar and zip files found
322      *  in the specified resource.
323      * @param lib the resource that contains the jar and/or zip files.
324      */
325     public void addJars(Resource lib)
326     {
327         if (lib.exists() && lib.isDirectory())
328         {
329             String[] files=lib.list();
330             for (int f=0;files!=null && f<files.length;f++)
331             {
332                 try 
333                 {
334                     Resource fn=lib.addPath(files[f]);
335                     if(LOG.isDebugEnabled())
336                         LOG.debug("addJar - {}", fn);
337                     String fnlc=fn.getName().toLowerCase(Locale.ENGLISH);
338                     // don't check if this is a directory, see Bug 353165
339                     if (isFileSupported(fnlc))
340                     {
341                         String jar=fn.toString();
342                         jar=StringUtil.replace(jar, ",", "%2C");
343                         jar=StringUtil.replace(jar, ";", "%3B");
344                         addClassPath(jar);
345                     }
346                 }
347                 catch (Exception ex)
348                 {
349                     LOG.warn(Log.EXCEPTION,ex);
350                 }
351             }
352         }
353     }
354 
355     /* ------------------------------------------------------------ */
356     @Override
357     public PermissionCollection getPermissions(CodeSource cs)
358     {
359         PermissionCollection permissions=_context.getPermissions();
360         PermissionCollection pc= (permissions == null) ? super.getPermissions(cs) : permissions;
361         return pc;
362     }
363 
364     /* ------------------------------------------------------------ */
365     @Override
366     public Enumeration<URL> getResources(String name) throws IOException
367     {
368         List<URL> from_parent = new ArrayList<>();
369         List<URL> from_webapp = new ArrayList<>();
370         
371         Enumeration<URL> urls = _parent.getResources(name);
372         while (urls!=null && urls.hasMoreElements())
373         {
374             URL url = urls.nextElement();
375             if (Boolean.TRUE.equals(__loadServerClasses.get()) || !_context.isServerResource(name,url))
376                 from_parent.add(url);
377         }
378 
379         urls = this.findResources(name);
380         while (urls!=null && urls.hasMoreElements())
381         {
382             URL url = urls.nextElement();
383             if (!_context.isSystemResource(name,url) || from_parent.isEmpty())
384                 from_webapp.add(url);
385         }
386 
387         List<URL> resources;
388         
389         if (_context.isParentLoaderPriority())
390         {
391             from_parent.addAll(from_webapp);
392             resources = from_parent;
393         }
394         else
395         {
396             from_webapp.addAll(from_parent);
397             resources = from_webapp;
398         }
399         
400         if (LOG.isDebugEnabled())
401             LOG.debug("getResources {} {}",name,resources);
402 
403         return Collections.enumeration(resources);
404     }
405     
406     /* ------------------------------------------------------------ */
407     /**
408      * Get a resource from the classloader
409      * 
410      * NOTE: this method provides a convenience of hacking off a leading /
411      * should one be present. This is non-standard and it is recommended 
412      * to not rely on this behavior
413      */
414     @Override
415     public URL getResource(String name)
416     {
417         URL resource=null;
418         if (_context.isParentLoaderPriority())
419         {
420             URL parent_url=_parent.getResource(name);
421             
422             // return if we have a url the webapp is allowed to see
423             if (parent_url!=null
424                 && (Boolean.TRUE.equals(__loadServerClasses.get()) 
425                     || !_context.isServerResource(name,parent_url)))
426                 resource = parent_url;
427             else
428             {
429                 URL webapp_url = this.findResource(name);
430 
431                 // If found here then OK to use regardless of system or server classes
432                 // If it is a system resource, we've already tried to load from parent, so
433                 // would have returned it.
434                 // If it is a server resource, doesn't matter as we have loaded it from the 
435                 // webapp
436                 if (webapp_url!=null)
437                     resource = webapp_url;
438             }
439         }
440         else
441         {
442             URL webapp_url = this.findResource(name);
443 
444             if (webapp_url!=null && !_context.isSystemResource(name,webapp_url))
445                 resource = webapp_url;
446             else
447             {
448 
449                 // Couldn't find or see a webapp resource, so try a parent
450                 URL parent_url=_parent.getResource(name);
451                 if (parent_url!=null
452                     && (Boolean.TRUE.equals(__loadServerClasses.get()) 
453                         || !_context.isServerResource(name,parent_url)))
454                     resource = parent_url;
455                 // We couldn't find a parent resource, so OK to return a webapp one if it exists 
456                 // and we just couldn't see it before 
457                 else if (webapp_url!=null)
458                     resource = webapp_url;
459             }
460         }
461         
462         // Perhaps this failed due to leading /
463         if (resource==null && name.startsWith("/"))
464             resource = getResource(name.substring(1));
465 
466         if (LOG.isDebugEnabled())
467             LOG.debug("getResource {} {}",name,resource);
468        
469         return resource;
470         
471     }
472 
473     /* ------------------------------------------------------------ */
474     @Override
475     protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException
476     {
477         synchronized (getClassLoadingLock(name))
478         {            
479             ClassNotFoundException ex= null;
480             Class<?> parent_class = null;
481             Class<?> webapp_class = null;
482             
483             // Has this loader loaded the class already?
484             webapp_class = findLoadedClass(name);
485             if (webapp_class != null)
486             {
487                 if (LOG.isDebugEnabled())
488                     LOG.debug("found webapp loaded {}",webapp_class);
489                 return webapp_class;
490             }
491             
492             // Should we try the parent loader first?
493             if (_context.isParentLoaderPriority())
494             {
495                 // Try the parent loader
496                 try
497                 {
498                     parent_class = _parent.loadClass(name); 
499 
500                     // If the webapp is allowed to see this class
501                     if (Boolean.TRUE.equals(__loadServerClasses.get()) || !_context.isServerClass(parent_class))
502                     {
503                         if (LOG.isDebugEnabled())
504                             LOG.debug("PLP parent loaded {}",parent_class);
505                         return parent_class;
506                     }
507                 }
508                 catch (ClassNotFoundException e)
509                 {
510                     // Save it for later
511                     ex = e;
512                 }
513                 
514                 // Try the webapp loader
515                 try
516                 {
517                     // If found here then OK to use regardless of system or server classes
518                     // If it is a system class, we've already tried to load from parent, so
519                     // would have returned it.
520                     // If it is a server class, doesn't matter as we have loaded it from the 
521                     // webapp
522                     webapp_class = this.findClass(name);
523                     resolveClass(webapp_class);
524                     if (LOG.isDebugEnabled())
525                         LOG.debug("PLP webapp loaded {}",webapp_class);
526                     return webapp_class;
527                 }
528                 catch (ClassNotFoundException e)
529                 {
530                     if (ex==null)
531                         ex = e;
532                     else
533                         ex.addSuppressed(e);
534                 }
535                 
536                 throw ex;
537             }
538             else
539             {
540                 // Not parent loader priority, so...
541 
542                 // Try the webapp classloader first
543                 // Look in the webapp classloader as a resource, to avoid 
544                 // loading a system class.
545                 String path = name.replace('.', '/').concat(".class");
546                 URL webapp_url = findResource(path);
547                 
548                 if (webapp_url!=null && !_context.isSystemResource(name,webapp_url))
549                 {
550                     webapp_class = this.foundClass(name,webapp_url);
551                     resolveClass(webapp_class);
552                     if (LOG.isDebugEnabled())
553                         LOG.debug("WAP webapp loaded {}",webapp_class);
554                     return webapp_class;
555                 }
556 
557                 // Try the parent loader
558                 try
559                 {
560                     parent_class = _parent.loadClass(name); 
561                     
562                     // If the webapp is allowed to see this class
563                     if (Boolean.TRUE.equals(__loadServerClasses.get()) || !_context.isServerClass(parent_class))
564                     {
565                         if (LOG.isDebugEnabled())
566                             LOG.debug("WAP parent loaded {}",parent_class);
567                         return parent_class;
568                     }
569                 }
570                 catch (ClassNotFoundException e)
571                 {
572                     ex=e;
573                 }
574 
575                 // We couldn't find a parent class, so OK to return a webapp one if it exists 
576                 // and we just couldn't see it before 
577                 if (webapp_url!=null)
578                 {
579                     webapp_class = this.foundClass(name,webapp_url);
580                     resolveClass(webapp_class);
581                     if (LOG.isDebugEnabled())
582                         LOG.debug("WAP !server webapp loaded {}",webapp_class);
583                     return webapp_class;
584                 }
585                 
586                 throw ex==null?new ClassNotFoundException(name):ex;
587             }
588         }
589     }
590 
591     /* ------------------------------------------------------------ */
592     /**
593      * @param transformer the transformer to add
594      * @deprecated {@link #addTransformer(ClassFileTransformer)} instead
595      */
596     @Deprecated
597     public void addClassFileTransformer(ClassFileTransformer transformer)
598     {
599         _transformers.add(transformer);
600     }
601     
602     /* ------------------------------------------------------------ */
603     /**
604      * @param transformer the transformer to remove
605      * @return true if transformer was removed
606      * @deprecated use {@link #removeTransformer(ClassFileTransformer)} instead
607      */
608     @Deprecated
609     public boolean removeClassFileTransformer(ClassFileTransformer transformer)
610     {
611         return _transformers.remove(transformer);
612     }
613 
614     /* ------------------------------------------------------------ */
615     public void addTransformer(ClassFileTransformer transformer)
616     {
617         _transformers.add(transformer);
618     }
619     
620     /* ------------------------------------------------------------ */
621     public boolean removeTransformer(ClassFileTransformer transformer)
622     {
623         return _transformers.remove(transformer);
624     }
625 
626     /* ------------------------------------------------------------ */
627     @Override
628     protected Class<?> findClass(final String name) throws ClassNotFoundException
629     {
630         if (_transformers.isEmpty())
631             return super.findClass(name);
632 
633         String path = name.replace('.', '/').concat(".class");
634         URL url = findResource(path);
635         if (url==null)
636             throw new ClassNotFoundException(name);
637         return foundClass(name,url);
638     }
639     
640     /* ------------------------------------------------------------ */
641     protected Class<?> foundClass(final String name, URL url) throws ClassNotFoundException
642     {
643         if (_transformers.isEmpty())
644             return super.findClass(name);
645 
646         InputStream content=null;
647         try
648         {
649             content = url.openStream();
650             byte[] bytes = IO.readBytes(content);
651 
652             if (LOG.isDebugEnabled())
653                 LOG.debug("foundClass({}) url={} cl={}",name,url,this);
654 
655             for (ClassFileTransformer transformer : _transformers)
656             {
657                 byte[] tmp = transformer.transform(this,name,null,null,bytes);
658                 if (tmp != null)
659                     bytes = tmp;
660             }
661 
662             return defineClass(name,bytes,0,bytes.length);
663         }
664         catch (IOException e)
665         {
666             throw new ClassNotFoundException(name,e);
667         }
668         catch (IllegalClassFormatException e)
669         {
670             throw new ClassNotFoundException(name,e);
671         }
672         finally
673         {
674             if (content!=null)
675             {
676                 try
677                 {
678                     content.close(); 
679                 }
680                 catch (IOException e)
681                 {
682                     throw new ClassNotFoundException(name,e);
683                 }
684             }
685         }
686     }
687 
688     
689     /* ------------------------------------------------------------ */
690     @Override
691     public void close() throws IOException
692     {
693         super.close();
694     }
695 
696     /* ------------------------------------------------------------ */
697     @Override
698     public String toString()
699     {
700         return "WebAppClassLoader=" + _name+"@"+Long.toHexString(hashCode());
701     }
702     
703 }