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