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 19package org.eclipse.jetty.webapp; 20 21import java.io.File; 22import java.io.IOException; 23import java.net.URL; 24import java.net.URLClassLoader; 25import java.security.CodeSource; 26import java.security.PermissionCollection; 27import java.util.ArrayList; 28import java.util.Collections; 29import java.util.Enumeration; 30import java.util.HashSet; 31import java.util.List; 32import java.util.Locale; 33import java.util.Set; 34import java.util.StringTokenizer; 35 36import org.eclipse.jetty.util.StringUtil; 37import org.eclipse.jetty.util.log.Log; 38import org.eclipse.jetty.util.log.Logger; 39import org.eclipse.jetty.util.resource.Resource; 40import 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 */ 61public 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} 450