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 }