blob: e64180db7c9bd338c2c6b964b651297180f6daa6 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.netbeans;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.openide.util.Enumerations;
import org.openide.util.Lookup;
/**
* A class loader that has multiple parents and uses them for loading
* classes and resources. It is optimized for working in the enviroment
* of a deeply nested classloader hierarchy. It uses shared knowledge
* about package population to route the loading request directly
* to the correct classloader.
* It doesn't load classes or resources itself, but allows subclasses
* to add such functionality.
*
* @author Petr Nejedly, Jesse Glick
*/
public class ProxyClassLoader extends ClassLoader {
private static final Logger LOGGER = Logger.getLogger(ProxyClassLoader.class.getName());
private static final boolean LOG_LOADING;
private static final ClassLoader TOP_CL = ProxyClassLoader.class.getClassLoader();
static {
boolean prop1 = System.getProperty("org.netbeans.ProxyClassLoader.level") != null;
LOG_LOADING = prop1 || LOGGER.isLoggable(Level.FINE);
}
/** All known packages
* @GuardedBy("packages")
*/
private final Map<String, Package> packages = new HashMap<String, Package>();
/** keeps information about parent classloaders, system classloader, etc.*/
volatile ProxyClassParents parents;
/** Create a multi-parented classloader.
* @param parents all direct parents of this classloader, except system one.
* @param transitive whether other PCLs depending on this one will
* automatically search through its parent list
*/
public ProxyClassLoader(ClassLoader[] parents, boolean transitive) {
super(TOP_CL);
this.parents = ProxyClassParents.coalesceParents(this, parents, TOP_CL, transitive);
}
protected final void addCoveredPackages(Iterable<String> coveredPackages) {
ProxyClassPackages.addCoveredPackages(this, coveredPackages);
}
// this is used only by system classloader, maybe we can redesign it a bit
// to live without this functionality, then destroy may also go away
/** Add new parents dynamically.
* @param nueparents the new parents to add (append to list)
* @throws IllegalArgumentException in case of a null or cyclic parent (duplicate OK)
*/
public void append(ClassLoader[] nueparents) throws IllegalArgumentException {
if (nueparents == null) throw new IllegalArgumentException("null parents array"); // NOI18N
for (ClassLoader cl : nueparents) {
if (cl == null) throw new IllegalArgumentException("null parent: " + Arrays.asList(nueparents)); // NOI18N
}
ProxyClassLoader[] resParents = null;
ModuleFactory moduleFactory = Lookup.getDefault().lookup(ModuleFactory.class);
if (moduleFactory != null && moduleFactory.removeBaseClassLoader()) {
// this hack is here to prevent having the application classloader
// as parent to all module classloaders.
parents = ProxyClassParents.coalesceParents(this, nueparents, ClassLoader.getSystemClassLoader(), parents.isTransitive());
} else {
parents = parents.append(this, nueparents);
}
}
/**
* Loads the class with the specified name. The implementation of
* this method searches for classes in the following order:<p>
* <ol>
* <li> Looks for a known package and pass the loading to the ClassLoader
for that package.
* <li> For unknown packages passes the call directly
* already been loaded.
* </ol>
*
* @param name the name of the class
* @param resolve if <code>true</code> then resolve the class
* @return the resulting <code>Class</code> object
* @exception ClassNotFoundException if the class could not be found
*/
@Override
protected synchronized Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
final Class<?> cls = doFindClass(name);
if (resolve) resolveClass(cls);
return cls;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
LOGGER.log(Level.FINEST, "{0} finding class {1}", new Object[] {this, name});
return doFindClass(name);
}
private Class<?> doFindClass(String name) throws ClassNotFoundException {
if (LOG_LOADING && !name.startsWith("java.")) {
LOGGER.log(Level.FINEST, "{0} initiated loading of {1}",
new Object[] {this, name});
}
Class<?> cls = null;
int last = name.lastIndexOf('.');
if (last == -1) {
throw new ClassNotFoundException("Will not load classes from default package (" + name + ")"); // NOI18N
}
// Strip+intern or use from package coverage
String pkg = (last >= 0) ? name.substring(0, last) : "";
final String path = pkg.replace('.', '/') + "/";
Set<ProxyClassLoader> del = ProxyClassPackages.findCoveredPkg(pkg);
Boolean boo = isSystemPackage(pkg);
if ((boo == null || boo.booleanValue()) && shouldDelegateResource(path, null)) {
try {
cls = parents.systemCL().loadClass(name);
if (boo == null) registerSystemPackage(pkg, true);
return cls; // try SCL first
} catch (ClassNotFoundException e) {
// No dissaster, try other loaders
}
}
if (del == null) {
// uncovered package, go directly to SCL (may throw the CNFE for us)
//if (shouldDelegateResource(path, null)) cls = par.systemCL().loadClass(name);
} else if (del.size() == 1) {
// simple package coverage
ProxyClassLoader pcl = del.iterator().next();
if (pcl == this || (parents.contains(pcl) && shouldDelegateResource(path, pcl))) {
cls = pcl.selfLoadClass(pkg, name);
if (cls != null) registerSystemPackage(pkg, false);
}/* else { // maybe it is also covered by SCL
if (shouldDelegateResource(path, null)) cls = par.systemCL().loadClass(name);
}*/
} else {
// multicovered package, search in order
for (ProxyClassLoader pcl : parents.loaders()) { // all our accessible parents
if (del.contains(pcl) && shouldDelegateResource(path, pcl)) { // that cover given package
Class<?> _cls = pcl.selfLoadClass(pkg, name);
if (_cls != null) {
if (cls == null) {
cls = _cls;
} else if (cls != _cls) {
String message = "Will not load class " + name + " arbitrarily from one of " +
cls.getClassLoader() + " and " + pcl + " starting from " + this +
"; see http://wiki.netbeans.org/DevFaqModuleCCE";
ClassNotFoundException cnfe = new ClassNotFoundException(message);
if (arbitraryLoadWarnings.add(message)) {
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.log(Level.FINE, null, cnfe);
} else {
LOGGER.warning(message);
}
}
throw cnfe;
}
}
}
}
if (cls == null && del.contains(this)) cls = selfLoadClass(pkg, name);
if (cls != null) registerSystemPackage(pkg, false);
}
if (cls == null && shouldDelegateResource(path, null)) {
try {
cls = parents.systemCL().loadClass(name);
} catch (ClassNotFoundException e) {
throw new ClassNotFoundException(diagnosticCNFEMessage(e.getMessage(), del), e);
}
}
if (cls == null) {
throw new ClassNotFoundException(diagnosticCNFEMessage(name, del));
}
return cls;
}
private String diagnosticCNFEMessage(String base, Set<ProxyClassLoader> del) {
String parentSetS;
int size = parents.size();
// Too big to show in its entirety - overwhelms the log file.
StringBuilder b = new StringBuilder();
b.append(base).append(" starting from ").append(this)
.append(" with possible defining loaders ").append(del)
.append(" and declared parents ");
Iterator<ProxyClassLoader> parentSetI = parents.loaders().iterator();
for (int i = 0; i < 10 && parentSetI.hasNext(); i++) {
b.append(i == 0 ? "[" : ", ");
b.append(parentSetI.next());
}
if (parentSetI.hasNext()) {
b.append(", ...").append(size - 10).append(" more");
}
b.append(']');
return b.toString();
}
private static final Set<String> arbitraryLoadWarnings = Collections.synchronizedSet(new HashSet<String>());
/** May return null */
private synchronized Class<?> selfLoadClass(String pkg, String name) {
Class<?> cls = findLoadedClass(name);
if (cls == null) {
try {
cls = doLoadClass(pkg, name);
} catch (NoClassDefFoundError e) {
// #145503: we can make a guess as to what triggered this error (since the JRE does not inform you).
// XXX Exceptions.attachMessage does not seem to work here
throw (NoClassDefFoundError) new NoClassDefFoundError(e.getMessage() + " while loading " + name +
"; see http://wiki.netbeans.org/DevFaqTroubleshootClassNotFound").initCause(e); // NOI18N
}
if (LOG_LOADING && !name.startsWith("java.")) LOGGER.log(Level.FINEST, "{0} loaded {1}",
new Object[] {this, name});
}
return cls;
}
/** This ClassLoader can't load anything itself. Subclasses
* may override this method to do some class loading themselves. The
* implementation should not throw any exception, just return
* <CODE>null</CODE> if it can't load required class.
*
* @param name the name of the class
* @return the resulting <code>Class</code> object or <code>null</code>
*/
protected Class<?> doLoadClass(String pkg, String name) {
return null;
}
private String stripInitialSlash(String resource) { // #90310
if (resource.startsWith("/")) {
LOGGER.log(Level.WARNING, "Should not use initial '/' in calls to ClassLoader.getResource(s): {0}", resource);
return resource.substring(1);
} else {
return resource;
}
}
/**
* Finds the resource with the given name.
* @param name a "/"-separated path name that identifies the resource.
* @return a URL for reading the resource, or <code>null</code> if
* the resource could not be found.
* @see #findResource(String)
*/
@Override
public final URL getResource(String name) {
return getResourceImpl(name);
}
URL getResourceImpl(String name) {
URL url = null;
name = stripInitialSlash(name);
int last = name.lastIndexOf('/');
String pkg;
String fallDef = null;
if (last >= 0) {
if (name.startsWith("META-INF/")) {
pkg = name.substring(8);
fallDef = name.substring(0, last).replace('/', '.');
} else {
pkg = name.substring(0, last).replace('/', '.');
}
} else {
pkg = "default/" + name;
fallDef = "";
}
String path = name.substring(0, last+1);
Boolean systemPackage = isSystemPackage(pkg);
if ((systemPackage == null || systemPackage) && shouldDelegateResource(path, null)) {
URL u = parents.systemCL().getResource(name);
if (u != null) {
if (systemPackage == null) {
registerSystemPackage(pkg, true);
}
return u;
}
// else try other loaders
}
Set<ProxyClassLoader> del = ProxyClassPackages.findCoveredPkg(pkg);
if (fallDef != null) {
Set<ProxyClassLoader> snd = ProxyClassPackages.findCoveredPkg(fallDef);
if (snd != null) {
if (del != null) {
del = new HashSet<ProxyClassLoader>(del);
del.addAll(snd);
} else {
del = snd;
}
}
}
if (del == null) {
// uncovered package, go directly to SCL
if (shouldDelegateResource(path, null)) url = parents.systemCL().getResource(name);
} else if (del.size() == 1) {
// simple package coverage
ProxyClassLoader pcl = del.iterator().next();
if (pcl == this || (parents.contains(pcl) && shouldDelegateResource(path, pcl)))
url = pcl.findResource(name);
} else {
// multicovered package, search in order
for (ProxyClassLoader pcl : parents.loaders()) { // all our accessible parents
if (del.contains(pcl) && shouldDelegateResource(path, pcl)) { // that cover given package
url = pcl.findResource(name);
if (url != null) break;
}
}
if (url == null && del.contains(this)) url = findResource(name);
}
// uncovered package, go directly to SCL
if (url == null && shouldDelegateResource(path, null)) url = parents.systemCL().getResource(name);
return url;
}
/** This ClassLoader can't load anything itself. Subclasses
* may override this method to do some resource loading themselves.
*
* @param name the resource name
* @return a URL for reading the resource, or <code>null</code>
* if the resource could not be found.
*/
@Override
public URL findResource(String name) {
return super.findResource(name);
}
@Override
public final Enumeration<URL> getResources(String name) throws IOException {
return getResourcesImpl(name);
}
synchronized Enumeration<URL> getResourcesImpl(String name) throws IOException {
name = stripInitialSlash(name);
final int slashIdx = name.lastIndexOf('/');
final String path = name.substring(0, slashIdx + 1);
String pkg;
String fallDef = null;
if (slashIdx >= 0) {
if (name.startsWith("META-INF/")) {
pkg = name.substring(8);
fallDef = name.substring(0, slashIdx).replace('/', '.');
} else {
pkg = name.substring(0, slashIdx).replace('/', '.');
}
} else {
pkg = "default/" + name;
fallDef = "";
}
List<Enumeration<URL>> sub = new ArrayList<Enumeration<URL>>();
// always consult SCL first
if (shouldDelegateResource(path, null)) sub.add(parents.systemCL().getResources(name));
Set<ProxyClassLoader> del = ProxyClassPackages.findCoveredPkg(pkg);
if (fallDef != null) {
Set<ProxyClassLoader> snd = ProxyClassPackages.findCoveredPkg(fallDef);
if (snd != null) {
if (del != null) {
del = new HashSet<ProxyClassLoader>(del);
del.addAll(snd);
} else {
del = snd;
}
}
}
if (del != null) {
for (ProxyClassLoader pcl : parents.loaders()) { // all our accessible parents
if (del.contains(pcl) && shouldDelegateResource(path, pcl)) { // that cover given package
sub.add(pcl.findResources(name));
}
}
if (del.contains(this)) {
sub.add(findResources(name));
}
}
// Should not be duplicates, assuming the parent loaders are properly distinct
// from one another and do not overlap in JAR usage, which they ought not.
// Anyway MetaInfServicesLookup, the most important client of this method, does
// its own duplicate filtering already.
return Enumerations.concat(Collections.enumeration(sub));
}
@Override
public Enumeration<URL> findResources(String name) throws IOException {
return super.findResources(name);
}
/**
* Returns a Package that has been defined by this class loader or any
* of its parents.
*
* @param name the package name
* @return the Package corresponding to the given name, or null if not found
*/
@Override
protected Package getPackage(String name) {
return getPackageFast(name, true);
}
/**
* Faster way to find a package.
* @param name package name in org.netbeans.modules.foo format
* @param sname package name in org/netbeans/modules/foo/ format
* @param recurse whether to also ask parents
* @return located package, or null
*/
protected Package getPackageFast(String name, boolean recurse) {
synchronized (packages) {
Package pkg = packages.get(name);
if (pkg != null) {
return pkg;
}
if (!recurse) {
return null;
}
String path = name.replace('.', '/');
for (ProxyClassLoader par : this.parents.loaders()) {
if (!shouldDelegateResource(path, par))
continue;
pkg = par.getPackageFast(name, false);
if (pkg != null) break;
}
// pretend the resource ends with "/". This works better with hidden package and
// prefix-based checks.
if (pkg == null && shouldDelegateResource(path + "/", null)) {
// Cannot access either Package.getSystemPackages nor ClassLoader.getPackage
// from here, so do the best we can though it will cause unnecessary
// duplication of the package cache (PCL.packages vs. CL.packages):
pkg = super.getPackage(name);
}
if (pkg != null) {
packages.put(name, pkg);
}
return pkg;
}
}
/** This is here just for locking serialization purposes.
* Delegates to super.definePackage with proper locking.
* Also tracks the package in our private cache, since
* getPackageFast(...,...,false) will not call super.getPackage.
*/
@Override
protected Package definePackage(String name, String specTitle,
String specVersion, String specVendor, String implTitle,
String implVersion, String implVendor, URL sealBase )
throws IllegalArgumentException {
synchronized (packages) {
Package pkg = super.definePackage(name, specTitle, specVersion, specVendor, implTitle,
implVersion, implVendor, sealBase);
packages.put(name, pkg);
return pkg;
}
}
/**
* Returns all of the Packages defined by this class loader and its parents.
*
* @return the array of <code>Package</code> objects defined by this
* <code>ClassLoader</code>
*/
@Override
protected synchronized Package[] getPackages() {
return getPackages(new HashSet<ClassLoader>());
}
/**
* Returns all of the Packages defined by this class loader and its parents.
* Do not recurse to parents in addedParents set. It speeds up execution
* time significantly.
* @return the array of <code>Package</code> objects defined by this
* <code>ClassLoader</code>
*/
private Package[] getPackages(Set<ClassLoader> addedParents) {
Map<String,Package> all = new HashMap<String, Package>();
// XXX call shouldDelegateResource on each?
addPackages(all, super.getPackages());
for (ClassLoader par : this.parents.loaders()) {
if (par instanceof ProxyClassLoader && addedParents.add(par)) {
// XXX should ideally use shouldDelegateResource here...
addPackages(all, ((ProxyClassLoader)par).getPackages(addedParents));
}
}
synchronized (packages) {
all.keySet().removeAll(packages.keySet());
packages.putAll(all);
return packages.values().toArray(new Package[packages.size()]);
}
}
private void addPackages(Map<String,Package> all, Package[] pkgs) {
// Would be easier if Package.equals() was just defined sensibly...
for (int i = 0; i < pkgs.length; i++) {
all.put(pkgs[i].getName(), pkgs[i]);
}
}
protected final void setSystemClassLoader(ClassLoader s) {
parents = parents.changeSystemClassLoader(s);
}
protected boolean shouldDelegateResource(String pkg, ClassLoader parent) {
return true;
}
/** Called before releasing the classloader so it can itself unregister
* from the global ClassLoader pool */
public void destroy() {
ProxyClassPackages.removeCoveredPakcages(this);
}
final ClassLoader firstParent() {
Iterator<ProxyClassLoader> it = parents.loaders().iterator();
return it.hasNext() ? it.next() : null;
}
//
// System Class Loader Packages Support
//
private static Map<String,Boolean> sclPackages = Collections.synchronizedMap(new HashMap<String,Boolean>());
private static Boolean isSystemPackage(String pkg) {
return sclPackages.get(pkg);
}
private static void registerSystemPackage(String pkg, boolean isSystemPkg) {
sclPackages.put(pkg, isSystemPkg);
}
}