/* * Copyright 2003-2011 JetBrains s.r.o. * * Licensed 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 jetbrains.mps.classloading; import jetbrains.mps.module.ReloadableModule; import jetbrains.mps.reloading.ClassBytesProvider.ClassBytes; import jetbrains.mps.util.NameUtil; import jetbrains.mps.util.ProtectionDomainUtil; import jetbrains.mps.util.iterable.IterableEnumeration; import org.apache.log4j.LogManager; import org.apache.log4j.Logger; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import sun.misc.CompoundEnumeration; import java.io.IOException; import java.net.URL; import java.security.ProtectionDomain; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Enumeration; import java.util.List; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; /** * MPS implementation of <code>java.lang.ClassLoader</code> which uses non-standard way of class loading delegation. * Its methods #loadClass, #findResources are called by JVM during JVM class loading process and also * by an explicit user call of #getClass and #getOwnClass methods in {@link ReloadableModule} and * in {@link ClassLoaderManager} instance (old deprecated way). * Note that these methods yield additional error information in the case of failure. * Users of class loading API are supposed to process it on their own behalf. * * @see jetbrains.mps.classloading.ModuleIsNotLoadableException * @see jetbrains.mps.classloading.ModuleClassNotFoundException * * @author apyshkin */ public final class ModuleClassLoader extends ClassLoader { private static final Logger LOG = LogManager.getLogger(ModuleClassLoader.class); private static final ClassLoader BOOTSTRAP_CLASSLOADER = Object.class.getClassLoader(); private final ModuleClassLoaderSupport mySupport; // null values are not allowed => using <code>Optional</code> private final ConcurrentMap<String, Optional<Class<?>>> myClasses = new ConcurrentHashMap<>(); private volatile Collection<ClassLoader> myDependenciesClassLoaders; private boolean myDisposed; private final Object myPackageLock = new Object(); /** * MPS has a cyclic delegation classloading model (module A.a1 triggers class B.b which in turn triggers the loading of * the class A.a2 in the case when A depends on B and vice versa; the implicit class loading is triggered in the #defineClass invocation * in the {@link #loadFromSelf(String)} method). * * Thus according to jls we declare it as a parallel capable. * Without this registration the threading model of the MPS classloading is flawed. * @since 3.4 */ static { if (!registerAsParallelCapable()) { LOG.error("Was not able to register the MPS class loader as parallel capable: one might encounter a deadlock", new Throwable()); } } public boolean isDisposed() { return myDisposed; } private void checkNotDisposed() throws ModuleClassLoaderIsDisposedException { if (isDisposed()) { throw new ModuleClassLoaderIsDisposedException(String.format("ClassLoader of the module '%s' is disposed and not operable!", getModule()), getModule()); } } public ModuleClassLoader(ModuleClassLoaderSupport support) { super(support.getModule().getRootClassLoader()); mySupport = support; } @NotNull public Class<?> loadOwnClass(String name) throws ClassNotFoundException { Class<?> aClass = loadClass(name, false, true); if (aClass == null) { throw new ModuleClassNotFoundException(getModule()); } return aClass; } ReloadableModule getModule() { return mySupport.getModule(); } @Override protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{ return loadClass(name, resolve, false); } private Class<?> loadClass(String fqName, boolean resolve, boolean onlyFromSelf) throws ClassNotFoundException { checkNotDisposed(); Class<?> aClass = getClassFromCache(fqName); if (aClass != null) return aClass; if (fqName.startsWith("java.")) { return Class.forName(fqName, false, BOOTSTRAP_CLASSLOADER); } aClass = loadFromSelf(fqName); if (aClass != null) { return aClass; } try { if (!onlyFromSelf) { aClass = loadFromDeps(fqName); } } finally { aClass = recordClass(fqName, aClass); } if (aClass == null) throw createCLNFException(fqName); if (resolve) resolveClass(aClass); return aClass; } /** * @return new class if there was no same class already defined * or an old class if there is another definition already recorded into the map. */ private Class<?> recordClass(String fqName, Class<?> aClass) { Optional<Class<?>> previousValue = myClasses.putIfAbsent(fqName, Optional.ofNullable(aClass)); if (previousValue != null) { // class has been already defined in a concurrent thread if (previousValue.isPresent()) { aClass = previousValue.get(); } else { aClass = null; } } return aClass; } private ModuleClassNotFoundException createCLNFException(String name) { ReloadableModule module = mySupport.getModule(); return new ModuleClassNotFoundException(module, String.format("Unable to load class: '%s' using ModuleClassLoader of the '%s' module", name, module.getModuleName())); } /** * @return null if there is no name in cache * @throws ClassNotFoundException if class has been found already and it was null */ private Class<?> getClassFromCache(String name) throws ClassNotFoundException { Optional<Class<?>> optionalClass = myClasses.get(name); if (optionalClass == null) { return null; } if (!optionalClass.isPresent()) { throw createCLNFException(name); } return optionalClass.get(); } private Class<?> loadFromSelf(String fqName) throws ClassNotFoundException { synchronized (getClassLoadingLock(fqName)) { Class<?> aClass = getClassFromCache(fqName); if (aClass != null) { return aClass; } ClassBytes classBytes = mySupport.findClassBytes(fqName); if (classBytes != null) { String pack = NameUtil.namespaceFromLongName(fqName); synchronized (myPackageLock) { if (getPackage(pack) == null) { definePackage(pack, null, null, null, null, null, null, null); } } ProtectionDomain newProtectionDomain = ProtectionDomainUtil.loadedClassDomain(classBytes.getPath()); byte[] bytes = classBytes.getBytes(); aClass = defineClass(fqName, bytes, 0, bytes.length, newProtectionDomain); return recordClass(fqName, aClass); } } return null; } private Class<?> loadFromDeps(String name) throws ClassNotFoundException { Collection<? extends ClassLoader> dependencyClassLoaders = getDependencyClassLoaders(); // loading from ModuleClassLoaders firstly; it's faster, we can tell right here if we can find class there. for (ClassLoader depCL : dependencyClassLoaders) { if (depCL instanceof ModuleClassLoader) { ModuleClassLoader depCL1 = (ModuleClassLoader) depCL; if (depCL1.mySupport.canFindClass(name)) { // here it will certainly load with class loader depCL return depCL1.loadFromSelf(name); } } } for (ClassLoader depCL : dependencyClassLoaders) { if (!(depCL instanceof ModuleClassLoader)) { try { return Class.forName(name, false, depCL); } catch (ClassNotFoundException ignored) { } } } if (dependencyClassLoaders.contains(getParent())) { return null; } else { return loadFromParent(name); } } private Class<?> loadFromParent(String name) throws ClassNotFoundException { ClassLoader parent = getParent(); if (parent == null) return null; return parent.loadClass(name); } @Nullable @Override public URL getResource(@NonNls String name) { URL ownResource = findResource(name); if (ownResource == null) { return getParent().getResource(name); } return ownResource; } @NotNull @Override public Enumeration<URL> getResources(@NonNls String name) throws IOException { Enumeration<URL> ownResources = findResources(name); Enumeration<URL> parentResources = getParent().getResources(name); //noinspection unchecked return new CompoundEnumeration<>(new Enumeration[]{ownResources, parentResources}); } @Override protected URL findResource(String name) { checkNotDisposed(); List<ClassLoader> classLoadersToCheck = new ArrayList<ClassLoader>(); classLoadersToCheck.add(this); classLoadersToCheck.addAll(getDependencyClassLoaders()); for (ClassLoader dep : classLoadersToCheck) { if (dep instanceof ModuleClassLoader) { URL res; try { res = ((ModuleClassLoader) dep).mySupport.findResource(name); } catch (ModuleIsNotLoadableException e) { throw new RuntimeException(e); } if (res != null) return res; } } return null; } @Override protected Enumeration<URL> findResources(String name) throws IOException { checkNotDisposed(); List<ClassLoader> classLoadersToCheck = new ArrayList<ClassLoader>(); classLoadersToCheck.add(this); classLoadersToCheck.addAll(getDependencyClassLoaders()); List<URL> result = new ArrayList<>(); for (ClassLoader dep : classLoadersToCheck) { if (dep instanceof ModuleClassLoader) { Enumeration<URL> resources; try { resources = ((ModuleClassLoader) dep).mySupport.findResources(name); } catch (ModuleIsNotLoadableException e) { throw new RuntimeException(e); } while (resources.hasMoreElements()) result.add(resources.nextElement()); } } return new IterableEnumeration<>(result); } /** * Note: the actual dispose is called asynchronously in the EDT. * The motive is to allow a ClassLoading client to dispose asynchronously in the Event Dispatch Thread. */ public void dispose() { myDisposed = true; myClasses.clear(); if (myDependenciesClassLoaders != null) { myDependenciesClassLoaders.clear(); } } /** * @return all dependencies [excluding itself] */ private Collection<ClassLoader> getDependencyClassLoaders() { if (myDependenciesClassLoaders == null) { myDependenciesClassLoaders = mySupport.getCompileDependencies(); } return myDependenciesClassLoaders; } public String toString() { return String.format("%s ModuleClassLoader %s", mySupport.getModule(), myDisposed ? "[DISPOSED]" : ""); } public static class ModuleClassLoaderIsDisposedException extends IllegalStateException { private final ReloadableModule myModule; private ModuleClassLoaderIsDisposedException(String msg, @NotNull ReloadableModule module) { super(msg); myModule = module; } public ReloadableModule getModule() { return myModule; } } }