All Downloads are FREE. Search and download functionalities are using the official Maven repository.

groovy.lang.GroovyClassLoader Maven / Gradle / Ivy

There is a newer version: 5.0.0-alpha-11
Show newest version
/*
 *  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.
 */
/*
 * @todo multi threaded compiling of the same class but with different roots
 * for compilation... T1 compiles A, which uses B, T2 compiles B... mark A and B
 * as parsed and then synchronize compilation. Problems: How to synchronize?
 * How to get error messages?
 *
 */
package groovy.lang;

import groovy.util.CharsetToolkit;
import org.codehaus.groovy.GroovyBugError;
import org.codehaus.groovy.ast.ClassHelper;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.ast.FieldNode;
import org.codehaus.groovy.ast.InnerClassNode;
import org.codehaus.groovy.ast.ModuleNode;
import org.codehaus.groovy.ast.expr.ConstantExpression;
import org.codehaus.groovy.classgen.GeneratorContext;
import org.codehaus.groovy.classgen.Verifier;
import org.codehaus.groovy.control.BytecodeProcessor;
import org.codehaus.groovy.control.CompilationFailedException;
import org.codehaus.groovy.control.CompilationUnit;
import org.codehaus.groovy.control.CompilePhase;
import org.codehaus.groovy.control.CompilerConfiguration;
import org.codehaus.groovy.control.Phases;
import org.codehaus.groovy.control.SourceUnit;
import org.codehaus.groovy.runtime.EncodingGroovyMethods;
import org.codehaus.groovy.runtime.IOGroovyMethods;
import org.codehaus.groovy.runtime.InvokerHelper;
import org.codehaus.groovy.runtime.memoize.EvictableCache;
import org.codehaus.groovy.runtime.memoize.StampedCommonCache;
import org.codehaus.groovy.runtime.memoize.UnlimitedConcurrentCache;
import org.codehaus.groovy.util.URLStreams;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Opcodes;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLClassLoader;
import java.net.URLConnection;
import java.net.URLDecoder;
import java.security.CodeSource;
import java.security.NoSuchAlgorithmException;
import java.security.Permission;
import java.security.PermissionCollection;
import java.security.Permissions;
import java.security.PrivilegedAction;
import java.security.ProtectionDomain;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Enumeration;
import java.util.Map;

/**
 * A ClassLoader which can load Groovy classes. The loaded classes are cached,
 * classes from other classloaders should not be cached. To be able to load a
 * script that was asked for earlier but was created later it is essential not
 * to keep anything like a "class not found" information for that class name.
 * This includes possible parent loaders. Classes that are not cached are always
 * reloaded.
 */
public class GroovyClassLoader extends URLClassLoader {
    private static final URL[] EMPTY_URL_ARRAY = new URL[0];
    private static final Class[] EMPTY_CLASS_ARRAY = new Class[0];

    /**
     * this cache contains the loaded classes or PARSING, if the class is currently parsed
     */
    protected final EvictableCache classCache = new UnlimitedConcurrentCache();

    /**
     * This cache contains mappings of file name to class. It is used
     * to bypass compilation.
     */
    protected final StampedCommonCache sourceCache = new StampedCommonCache();

    private final CompilerConfiguration config;
    private String sourceEncoding;
    private Boolean recompile;
    // use 1000000 as offset to avoid conflicts with names from the GroovyShell
    private static int scriptNameCounter = 1000000;

    private GroovyResourceLoader resourceLoader = new GroovyResourceLoader() {
        @Override
        @SuppressWarnings("removal") // TODO a future Groovy version should perform the operation not as a privileged action
        public URL loadGroovySource(final String filename) throws MalformedURLException {
            return java.security.AccessController.doPrivileged((PrivilegedAction) () -> {
                for (String extension : config.getScriptExtensions()) {
                    try {
                        URL ret = getSourceFile(filename, extension);
                        if (ret != null)
                            return ret;
                    } catch (Throwable t) {
                    }
                }
                return null;
            });
        }
    };

    /**
     * creates a GroovyClassLoader using the current Thread's context
     * Class loader as parent.
     */
    public GroovyClassLoader() {
        this(Thread.currentThread().getContextClassLoader());
    }

    /**
     * creates a GroovyClassLoader using the given ClassLoader as parent
     */
    public GroovyClassLoader(ClassLoader loader) {
        this(loader, null);
    }

    /**
     * creates a GroovyClassLoader using the given GroovyClassLoader as parent.
     * This loader will get the parent's CompilerConfiguration
     */
    public GroovyClassLoader(GroovyClassLoader parent) {
        this(parent, parent.config, false);
    }

    /**
     * creates a GroovyClassLoader.
     *
     * @param parent                    the parent class loader
     * @param config                    the compiler configuration
     * @param useConfigurationClasspath determines if the configurations classpath should be added
     */
    public GroovyClassLoader(ClassLoader parent, CompilerConfiguration config, boolean useConfigurationClasspath) {
        super(EMPTY_URL_ARRAY, parent);
        if (config == null) config = CompilerConfiguration.DEFAULT;
        this.config = config;
        if (useConfigurationClasspath) {
            for (String path : config.getClasspath()) {
                this.addClasspath(path);
            }
        }

        initSourceEncoding(config);
    }

    private void initSourceEncoding(CompilerConfiguration config) {
        sourceEncoding = config.getSourceEncoding();
        if (null == sourceEncoding) {
            // Keep the same default source encoding with the one used by #parseClass(InputStream, String)
            // TODO should we use org.codehaus.groovy.control.CompilerConfiguration.DEFAULT_SOURCE_ENCODING instead?
            sourceEncoding = CharsetToolkit.getDefaultSystemCharset().name();
        }
    }

    /**
     * creates a GroovyClassLoader using the given ClassLoader as parent.
     */
    public GroovyClassLoader(ClassLoader loader, CompilerConfiguration config) {
        this(loader, config, true);
    }

    public void setResourceLoader(GroovyResourceLoader resourceLoader) {
        if (resourceLoader == null) {
            throw new IllegalArgumentException("Resource loader must not be null!");
        }
        this.resourceLoader = resourceLoader;
    }

    public GroovyResourceLoader getResourceLoader() {
        return resourceLoader;
    }

    /**
     * Loads the given class node returning the implementation Class.
     * 

* WARNING: this compilation is not synchronized * * @param classNode * @return a class */ public Class defineClass(ClassNode classNode, String file, String newCodeBase) { CodeSource codeSource = null; try { codeSource = new CodeSource(new URL("file", "", newCodeBase), (java.security.cert.Certificate[]) null); } catch (MalformedURLException e) { //swallow } CompilationUnit unit = createCompilationUnit(config, codeSource); ClassCollector collector = createCollector(unit, classNode.getModule().getContext()); try { unit.addClassNode(classNode); unit.setClassgenCallback(collector); unit.compile(Phases.CLASS_GENERATION); definePackageInternal(collector.generatedClass.getName()); return collector.generatedClass; } catch (CompilationFailedException e) { throw new RuntimeException(e); } } /** * Check if this class loader has compatible {@link CompilerConfiguration} * with the provided one. * @param config the compiler configuration to test for compatibility * @return {@code true} if the provided config is exactly the same instance * of {@link CompilerConfiguration} as this loader has */ public boolean hasCompatibleConfiguration(CompilerConfiguration config) { return this.config == config; } /** * Parses the given file into a Java class capable of being run * * @param file the file name to parse * @return the main class defined in the given script */ public Class parseClass(File file) throws CompilationFailedException, IOException { return parseClass(new GroovyCodeSource(file, config.getSourceEncoding())); } /** * Parses the given text into a Java class capable of being run * * @param text the text of the script/class to parse * @param fileName the file name to use as the name of the class * @return the main class defined in the given script */ public Class parseClass(final String text, final String fileName) throws CompilationFailedException { GroovyCodeSource gcs = createCodeSource((PrivilegedAction) () -> new GroovyCodeSource(text, fileName, "/groovy/script")); gcs.setCachable(false); return parseClass(gcs); } @SuppressWarnings("removal") // TODO a future Groovy version should perform the operation not as a privileged action private GroovyCodeSource createCodeSource(PrivilegedAction action) { return java.security.AccessController.doPrivileged(action); } /** * Parses the given text into a Java class capable of being run * * @param text the text of the script/class to parse * @return the main class defined in the given script */ public Class parseClass(String text) throws CompilationFailedException { try { return parseClass(text, "Script_" + EncodingGroovyMethods.md5(text) + ".groovy"); } catch (NoSuchAlgorithmException e) { throw new GroovyBugError("Failed to generate md5", e); // should never happen } } public synchronized String generateScriptName() { scriptNameCounter++; return "script" + scriptNameCounter + ".groovy"; } public Class parseClass(final Reader reader, final String fileName) throws CompilationFailedException { GroovyCodeSource gcs = createCodeSource((PrivilegedAction) () -> { try { String scriptText = IOGroovyMethods.getText(reader); return new GroovyCodeSource(scriptText, fileName, "/groovy/script"); } catch (IOException e) { throw new RuntimeException("Impossible to read the content of the reader for file named: " + fileName, e); } }); return parseClass(gcs); } public Class parseClass(GroovyCodeSource codeSource) throws CompilationFailedException { return parseClass(codeSource, codeSource.isCachable()); } /** * Parses the given code source into a Java class. If there is a class file * for the given code source, then no parsing is done, instead the cached class is returned. * * @param shouldCacheSource if true then the generated class will be stored in the source cache * @return the main class defined in the given script */ public Class parseClass(final GroovyCodeSource codeSource, boolean shouldCacheSource) throws CompilationFailedException { // it's better to cache class instances by the source code // GCL will load the unique class instance for the same source code // and avoid occupying Permanent Area/Metaspace repeatedly String cacheKey = genSourceCacheKey(codeSource); return sourceCache.getAndPut( cacheKey, key -> doParseClass(codeSource), shouldCacheSource ); } private String genSourceCacheKey(GroovyCodeSource codeSource) { StringBuilder strToDigest; String scriptText = codeSource.getScriptText(); if (null != scriptText) { strToDigest = new StringBuilder((int) (scriptText.length() * 1.2)); strToDigest.append("scriptText:").append(scriptText); CodeSource cs = codeSource.getCodeSource(); if (null != cs) { strToDigest.append("/codeSource:").append(cs); } } else { strToDigest = new StringBuilder(32); // if the script text is null, i.e. the script content is invalid // use the name as cache key for the time being to trigger the validation by `groovy.lang.GroovyClassLoader.validate` // note: the script will not be cached due to the invalid script content, // so it does not matter even if cache key is not the md5 value of script content strToDigest.append("name:").append(codeSource.getName()); } try { return EncodingGroovyMethods.md5(strToDigest); } catch (NoSuchAlgorithmException e) { throw new GroovyRuntimeException(e); // should never reach here! } } private Class doParseClass(GroovyCodeSource codeSource) { validate(codeSource); Class answer; // Was neither already loaded nor compiling, so compile and add to cache. CompilationUnit unit = createCompilationUnit(config, codeSource.getCodeSource()); if (recompile!=null && recompile || recompile==null && config.getRecompileGroovySource()) { unit.addFirstPhaseOperation(TimestampAdder.INSTANCE, CompilePhase.CLASS_GENERATION.getPhaseNumber()); } SourceUnit su = null; File file = codeSource.getFile(); if (file != null) { su = unit.addSource(file); } else { URL url = codeSource.getURL(); if (url != null) { su = unit.addSource(url); } else { su = unit.addSource(codeSource.getName(), codeSource.getScriptText()); } } ClassCollector collector = createCollector(unit, su); unit.setClassgenCallback(collector); int goalPhase = Phases.CLASS_GENERATION; if (config != null && config.getTargetDirectory() != null) goalPhase = Phases.OUTPUT; unit.compile(goalPhase); answer = collector.generatedClass; String mainClass = su.getAST().getMainClassName(); for (Object o : collector.getLoadedClasses()) { Class clazz = (Class) o; String clazzName = clazz.getName(); definePackageInternal(clazzName); setClassCacheEntry(clazz); if (clazzName.equals(mainClass)) answer = clazz; } return answer; } private static void validate(GroovyCodeSource codeSource) { if (codeSource.getFile() == null) { if (codeSource.getScriptText() == null) { throw new IllegalArgumentException("Script text to compile cannot be null!"); } } } @SuppressWarnings("deprecation") // TODO replace getPackage with getDefinedPackage once min JDK version >= 9 private void definePackageInternal(String className) { int i = className.lastIndexOf('.'); if (i != -1) { String pkgName = className.substring(0, i); java.lang.Package pkg = getPackage(pkgName); if (pkg == null) { definePackage(pkgName, null, null, null, null, null, null, null); } } } /** * gets the currently used classpath. * * @return a String[] containing the file information of the urls * @see #getURLs() */ protected String[] getClassPath() { //workaround for Groovy-835 URL[] urls = getURLs(); String[] ret = new String[urls.length]; for (int i = 0; i < ret.length; i++) { ret[i] = urls[i].getFile(); } return ret; } @Override protected PermissionCollection getPermissions(CodeSource codeSource) { PermissionCollection perms; try { try { perms = super.getPermissions(codeSource); } catch (SecurityException e) { // We lied about our CodeSource and that makes URLClassLoader unhappy. perms = new Permissions(); } ProtectionDomain myDomain = getProtectionDomain(); PermissionCollection myPerms = myDomain.getPermissions(); if (myPerms != null) { for (Enumeration elements = myPerms.elements(); elements.hasMoreElements();) { perms.add(elements.nextElement()); } } } catch (Throwable e) { // We lied about our CodeSource and that makes URLClassLoader unhappy. perms = new Permissions(); } perms.setReadOnly(); return perms; } @SuppressWarnings("removal") // TODO a future Groovy version should perform the operation not as a privileged action private ProtectionDomain getProtectionDomain() { return java.security.AccessController.doPrivileged(new PrivilegedAction() { @Override public ProtectionDomain run() { return getClass().getProtectionDomain(); } }); } public static class InnerLoader extends GroovyClassLoader { private final GroovyClassLoader delegate; private final long timeStamp; public InnerLoader(GroovyClassLoader delegate) { super(delegate); this.delegate = delegate; timeStamp = System.currentTimeMillis(); } @Override public void addClasspath(String path) { delegate.addClasspath(path); } @Override public void clearCache() { delegate.clearCache(); } @Override public URL findResource(String name) { return delegate.findResource(name); } @Override public Enumeration findResources(String name) throws IOException { return delegate.findResources(name); } @Override public Class[] getLoadedClasses() { return delegate.getLoadedClasses(); } @Override public URL getResource(String name) { return delegate.getResource(name); } @Override public InputStream getResourceAsStream(String name) { return delegate.getResourceAsStream(name); } @Override public GroovyResourceLoader getResourceLoader() { return delegate.getResourceLoader(); } @Override public URL[] getURLs() { return delegate.getURLs(); } @Override public Class loadClass(String name, boolean lookupScriptFiles, boolean preferClassOverScript, boolean resolve) throws ClassNotFoundException, CompilationFailedException { Class c = findLoadedClass(name); if (c != null) return c; return delegate.loadClass(name, lookupScriptFiles, preferClassOverScript, resolve); } @Override public Class parseClass(GroovyCodeSource codeSource, boolean shouldCache) throws CompilationFailedException { return delegate.parseClass(codeSource, shouldCache); } @Override public void setResourceLoader(GroovyResourceLoader resourceLoader) { delegate.setResourceLoader(resourceLoader); } @Override public void addURL(URL url) { delegate.addURL(url); } @Override public Class defineClass(ClassNode classNode, String file, String newCodeBase) { return delegate.defineClass(classNode, file, newCodeBase); } @Override public Class parseClass(File file) throws CompilationFailedException, IOException { return delegate.parseClass(file); } @Override public Class parseClass(String text, String fileName) throws CompilationFailedException { return delegate.parseClass(text, fileName); } @Override public Class parseClass(String text) throws CompilationFailedException { return delegate.parseClass(text); } @Override public String generateScriptName() { return delegate.generateScriptName(); } @Override public Class parseClass(Reader reader, String fileName) throws CompilationFailedException { return delegate.parseClass(reader, fileName); } @Override public Class parseClass(GroovyCodeSource codeSource) throws CompilationFailedException { return delegate.parseClass(codeSource); } @Override public Class defineClass(String name, byte[] b) { return delegate.defineClass(name, b); } @Override public Class loadClass(String name, boolean lookupScriptFiles, boolean preferClassOverScript) throws ClassNotFoundException, CompilationFailedException { return delegate.loadClass(name, lookupScriptFiles, preferClassOverScript); } @Override public void setShouldRecompile(Boolean mode) { delegate.setShouldRecompile(mode); } @Override public Boolean isShouldRecompile() { return delegate.isShouldRecompile(); } @Override public Class loadClass(String name) throws ClassNotFoundException { return delegate.loadClass(name); } @Override public Enumeration getResources(String name) throws IOException { return delegate.getResources(name); } @Override public void setDefaultAssertionStatus(boolean enabled) { delegate.setDefaultAssertionStatus(enabled); } @Override public void setPackageAssertionStatus(String packageName, boolean enabled) { delegate.setPackageAssertionStatus(packageName, enabled); } @Override public void setClassAssertionStatus(String className, boolean enabled) { delegate.setClassAssertionStatus(className, enabled); } @Override public void clearAssertionStatus() { delegate.clearAssertionStatus(); } @Override public void close() throws IOException { try { super.close(); } finally { delegate.close(); } } public long getTimeStamp() { return timeStamp; } } /** * creates a new CompilationUnit. If you want to add additional * phase operations to the CompilationUnit (for example to inject * additional methods, variables, fields), then you should overwrite * this method. * * @param config the compiler configuration, usually the same as for this class loader * @param source the source containing the initial file to compile, more files may follow during compilation * @return the CompilationUnit */ protected CompilationUnit createCompilationUnit(CompilerConfiguration config, CodeSource source) { return new CompilationUnit(config, source, this); } /** * creates a ClassCollector for a new compilation. * * @param unit the compilationUnit * @param su the SourceUnit * @return the ClassCollector */ protected ClassCollector createCollector(CompilationUnit unit, SourceUnit su) { return new ClassCollector(createLoader(), unit, su); } @SuppressWarnings("removal") // TODO a future Groovy version should perform the operation not as a privileged action private InnerLoader createLoader() { return java.security.AccessController.doPrivileged((PrivilegedAction) () -> new InnerLoader(GroovyClassLoader.this)); } public static class ClassCollector implements CompilationUnit.ClassgenCallback { private Class generatedClass; private final GroovyClassLoader cl; private final SourceUnit su; private final CompilationUnit unit; private final Collection loadedClasses; protected ClassCollector(InnerLoader cl, CompilationUnit unit, SourceUnit su) { this.cl = cl; this.unit = unit; this.loadedClasses = new ArrayList(); this.su = su; } public GroovyClassLoader getDefiningClassLoader() { return cl; } protected Class createClass(byte[] code, ClassNode classNode) { BytecodeProcessor bytecodePostprocessor = unit.getConfiguration().getBytecodePostprocessor(); byte[] fcode = code; if (bytecodePostprocessor!=null) { fcode = bytecodePostprocessor.processBytecode(classNode.getName(), fcode); } GroovyClassLoader cl = getDefiningClassLoader(); Class theClass = cl.defineClass(classNode.getName(), fcode, 0, fcode.length, unit.getAST().getCodeSource()); this.loadedClasses.add(theClass); if (generatedClass == null) { ModuleNode mn = classNode.getModule(); SourceUnit msu = null; if (mn != null) msu = mn.getContext(); ClassNode main = null; if (mn != null) main = mn.getClasses().get(0); if (msu == su && main == classNode) generatedClass = theClass; } return theClass; } protected Class onClassNode(ClassWriter classWriter, ClassNode classNode) { byte[] code = classWriter.toByteArray(); return createClass(code, classNode); } @Override public void call(ClassVisitor classWriter, ClassNode classNode) { onClassNode((ClassWriter) classWriter, classNode); } public Collection getLoadedClasses() { return this.loadedClasses; } } /** * Converts an array of bytes into an instance of {@code Class}. */ public Class defineClass(final String name, final byte[] bytes) throws ClassFormatError { return super.defineClass(name, bytes, 0, bytes.length); } /** * loads a class from a file or a parent classloader. * This method does call loadClass(String, boolean, boolean, boolean) * with the last parameter set to false. * * @throws CompilationFailedException if compilation was not successful */ public Class loadClass(final String name, boolean lookupScriptFiles, boolean preferClassOverScript) throws ClassNotFoundException, CompilationFailedException { return loadClass(name, lookupScriptFiles, preferClassOverScript, false); } /** * gets a class from the class cache. This cache contains only classes loaded through * this class loader or an InnerLoader instance. If no class is stored for a * specific name, then the method should return null. * * @param name of the class * @return the class stored for the given name * @see #removeClassCacheEntry(String) * @see #setClassCacheEntry(Class) * @see #clearCache() */ protected Class getClassCacheEntry(String name) { return classCache.get(name); } /** * sets an entry in the class cache. * * @param cls the class * @see #removeClassCacheEntry(String) * @see #getClassCacheEntry(String) * @see #clearCache() */ protected void setClassCacheEntry(Class cls) { classCache.put(cls.getName(), cls); } /** * removes a class from the class cache. * * @param name of the class * @see #getClassCacheEntry(String) * @see #setClassCacheEntry(Class) * @see #clearCache() */ protected void removeClassCacheEntry(String name) { classCache.remove(name); } /** * adds a URL to the classloader. * * @param url the new classpath element */ @Override public void addURL(URL url) { super.addURL(url); } /** * Indicates if a class is recompilable. Recompilable means, that the classloader * will try to locate a groovy source file for this class and then compile it again, * adding the resulting class as entry to the cache. Giving null as class is like a * recompilation, so the method should always return true here. Only classes that are * implementing GroovyObject are compilable and only if the timestamp in the class * is lower than Long.MAX_VALUE. *

* NOTE: First the parent loaders will be asked and only if they don't return a * class the recompilation will happen. Recompilation also only happen if the source * file is newer. * * @param cls the class to be tested. If null the method should return true * @return true if the class should be compiled again * @see #isSourceNewer(URL, Class) */ protected boolean isRecompilable(Class cls) { if (cls == null) return true; if (cls.getClassLoader() == this) return false; if (recompile == null && !config.getRecompileGroovySource()) return false; if (recompile != null && !recompile) return false; if (!GroovyObject.class.isAssignableFrom(cls)) return false; long timestamp = getTimeStamp(cls); return timestamp != Long.MAX_VALUE; } /** * sets if the recompilation should be enable. There are 3 possible * values for this. Any value different than null overrides the * value from the compiler configuration. true means to recompile if needed * false means to never recompile. * * @param mode the recompilation mode * @see CompilerConfiguration */ public void setShouldRecompile(Boolean mode) { recompile = mode; } /** * gets the currently set recompilation mode. null means, the * compiler configuration is used. False means no recompilation and * true means that recompilation will be done if needed. * * @return the recompilation mode */ public Boolean isShouldRecompile() { return recompile; } /** * loads a class from a file or a parent classloader. * * @param name of the class to be loaded * @param lookupScriptFiles if false no lookup at files is done at all * @param preferClassOverScript if true the file lookup is only done if there is no class * @param resolve see {@link java.lang.ClassLoader#loadClass(java.lang.String, boolean)} * @return the class found or the class created from a file lookup * @throws ClassNotFoundException if the class could not be found * @throws CompilationFailedException if the source file could not be compiled */ @SuppressWarnings("removal") // TODO a future Groovy version should remove the security check public Class loadClass(final String name, boolean lookupScriptFiles, boolean preferClassOverScript, boolean resolve) throws ClassNotFoundException, CompilationFailedException { // look into cache Class cls = getClassCacheEntry(name); // enable recompilation? boolean recompile = isRecompilable(cls); if (!recompile) return cls; // try parent loader ClassNotFoundException last = null; try { Class parentClassLoaderClass = super.loadClass(name, resolve); // always return if the parent loader was successful if (cls != parentClassLoaderClass) return parentClassLoaderClass; } catch (ClassNotFoundException cnfe) { last = cnfe; } catch (NoClassDefFoundError ncdfe) { if (ncdfe.getMessage().indexOf("wrong name") > 0) { last = new ClassNotFoundException(name); } else { throw ncdfe; } } // check security manager SecurityManager sm = System.getSecurityManager(); if (sm != null) { String className = name.replace('/', '.'); int i = className.lastIndexOf('.'); // no checks on the sun.reflect classes for reflection speed-up // in particular ConstructorAccessorImpl, MethodAccessorImpl, FieldAccessorImpl and SerializationConstructorAccessorImpl // which are generated at runtime by the JDK if (i != -1 && !className.startsWith("sun.reflect.")) { sm.checkPackageAccess(className.substring(0, i)); } } // prefer class if no recompilation if (cls != null && preferClassOverScript) return cls; // at this point the loading from a parent loader failed // and we want to recompile if needed. if (lookupScriptFiles) { // try groovy file try { // check if recompilation already happened. final Class classCacheEntry = getClassCacheEntry(name); if (classCacheEntry != cls) return classCacheEntry; URL source = resourceLoader.loadGroovySource(name); // if recompilation fails, we want cls==null Class oldClass = cls; cls = null; cls = recompile(source, name, oldClass); } catch (IOException ioe) { last = new ClassNotFoundException("IOException while opening groovy source: " + name, ioe); } finally { if (cls == null) { removeClassCacheEntry(name); } else { setClassCacheEntry(cls); } } } if (cls == null) { // no class found, there should have been an exception before now if (last == null) throw new AssertionError(true); throw last; } return cls; } /** * (Re)Compiles the given source. * This method starts the compilation of a given source, if * the source has changed since the class was created. For * this isSourceNewer is called. * * @param source the source pointer for the compilation * @param className the name of the class to be generated * @param oldClass a possible former class * @return the old class if the source wasn't new enough, the new class else * @throws CompilationFailedException if the compilation failed * @throws IOException if the source is not readable * @see #isSourceNewer(URL, Class) */ protected Class recompile(URL source, String className, Class oldClass) throws CompilationFailedException, IOException { if (source != null) { // found a source, compile it if newer if (oldClass == null || isSourceNewer(source, oldClass)) { String name = source.toExternalForm(); sourceCache.remove(name); if (isFile(source)) { try { return parseClass(new GroovyCodeSource(new File(source.toURI()), sourceEncoding)); } catch (URISyntaxException e) { // do nothing and fall back to the other version } } return parseClass(new InputStreamReader(URLStreams.openUncachedStream(source), sourceEncoding), name); } } return oldClass; } @Override public Class loadClass(String name) throws ClassNotFoundException { return loadClass(name, false); } /** * Implemented here to check package access prior to returning an * already loaded class. * * @throws CompilationFailedException if the compilation failed * @throws ClassNotFoundException if the class was not found * @see java.lang.ClassLoader#loadClass(java.lang.String, boolean) */ @Override protected Class loadClass(final String name, boolean resolve) throws ClassNotFoundException { return loadClass(name, true, true, resolve); } /** * gets the time stamp of a given class. For groovy * generated classes this usually means to return the value * of the static field __timeStamp. If the parameter doesn't * have such a field, then Long.MAX_VALUE is returned * * @param cls the class * @return the time stamp */ protected long getTimeStamp(Class cls) { return Verifier.getTimestamp(cls); } /** * This method will take a file name and try to "decode" any URL encoded characters. For example * if the file name contains any spaces this method call will take the resulting %20 encoded values * and convert them to spaces. *

* This method was added specifically to fix defect: Groovy-1787. The defect involved a situation * where two scripts were sitting in a directory with spaces in its name. The code would fail * when the class loader tried to resolve the file name and would choke on the URLEncoded space values. */ private static String decodeFileName(String fileName) { String decodedFile = fileName; try { decodedFile = URLDecoder.decode(fileName, "UTF-8"); } catch (UnsupportedEncodingException e) { System.err.println("Encountered an invalid encoding scheme when trying to use URLDecoder.decode() inside of the GroovyClassLoader.decodeFileName() method. Returning the unencoded URL."); System.err.println("Please note that if you encounter this error and you have spaces in your directory you will run into issues. Refer to GROOVY-1787 for description of this bug."); } return decodedFile; } private static boolean isFile(URL ret) { return ret != null && ret.getProtocol().equals("file"); } private static File getFileForUrl(URL ret, String filename) { String fileWithoutPackage = filename; if (fileWithoutPackage.indexOf('/') != -1) { int index = fileWithoutPackage.lastIndexOf('/'); fileWithoutPackage = fileWithoutPackage.substring(index + 1); } return fileReallyExists(ret, fileWithoutPackage); } private static File fileReallyExists(URL ret, String fileWithoutPackage) { File path; try { /* fix for GROOVY-5809 */ path = new File(ret.toURI()); } catch (URISyntaxException e) { path = new File(decodeFileName(ret.getFile())); } path = path.getParentFile(); File file = new File(path, fileWithoutPackage); if (file.exists()) { // file.exists() might be case insensitive. // Let's do case sensitive match for the filename try { String caseSensitiveName = file.getCanonicalPath(); int index = caseSensitiveName.lastIndexOf(File.separator); if (index != -1) { caseSensitiveName = caseSensitiveName.substring(index + 1); } if (fileWithoutPackage.equals(caseSensitiveName)) { return file; } } catch (IOException ignore) { // assume doesn't really exist if we can't read the file } } // file does not exist! return null; } private URL getSourceFile(String name, String extension) { String filename = name.replace('.', '/') + "." + extension; URL ret = getResource(filename); if (isFile(ret) && getFileForUrl(ret, filename) == null) return null; return ret; } /** * Decides if the given source is newer than a class. * * @param source the source we may want to compile * @param cls the former class * @return true if the source is newer, false else * @throws IOException if it is not possible to open an * connection for the given source * @see #getTimeStamp(Class) */ protected boolean isSourceNewer(URL source, Class cls) throws IOException { long lastMod; // Special handling for file:// protocol, as getLastModified() often reports // incorrect results (-1) if (isFile(source)) { // Coerce the file URL to a File // See ClassNodeResolver.isSourceNewer for another method that replaces '|' with ':'. // WTF: Why is this done and where is it documented? String path = source.getPath().replace('/', File.separatorChar).replace('|', ':'); File file = new File(path); lastMod = file.lastModified(); } else { URLConnection conn = source.openConnection(); lastMod = conn.getLastModified(); conn.getInputStream().close(); } long classTime = getTimeStamp(cls); return classTime + config.getMinimumRecompilationInterval() < lastMod; } /** * adds a classpath to this classloader. * * @param path is a jar file or a directory. * @see #addURL(URL) */ @SuppressWarnings("removal") // TODO a future Groovy version should perform the operation not as a privileged action public void addClasspath(final String path) { java.security.AccessController.doPrivileged((PrivilegedAction) () -> { URI newURI; try { newURI = new URI(path); // check if we can create a URL from that URI newURI.toURL(); } catch (URISyntaxException | IllegalArgumentException | MalformedURLException e) { // the URI has a false format, so lets try it with files ... newURI=new File(path).toURI(); } URL[] urls = getURLs(); for (URL url : urls) { // Do not use URL.equals. It uses the network to resolve names and compares ip addresses! // That is a violation of RFC and just plain evil. // http://michaelscharf.blogspot.com/2006/11/javaneturlequals-and-hashcode-make.html // http://docs.oracle.com/javase/7/docs/api/java/net/URL.html#equals(java.lang.Object) // "Since hosts comparison requires name resolution, this operation is a blocking operation. // Note: The defined behavior for equals is known to be inconsistent with virtual hosting in HTTP." try { if (newURI.equals(url.toURI())) return null; } catch (URISyntaxException e) { // fail fast! if we got a malformed URI the Classloader has to tell it throw new RuntimeException( e ); } } try { addURL(newURI.toURL()); } catch (MalformedURLException e) { // fail fast! if we got a malformed URL the Classloader has to tell it throw new RuntimeException( e ); } return null; }); } /** *

Returns all Groovy classes loaded by this class loader. * * @return all classes loaded by this class loader */ public Class[] getLoadedClasses() { return classCache.values().toArray(EMPTY_CLASS_ARRAY); } /** * Removes all classes from the class cache. *

* In addition to internal caches this method also clears any * previously set MetaClass information for the given set of * classes being removed. * * @see #getClassCacheEntry(String) * @see #setClassCacheEntry(Class) * @see #removeClassCacheEntry(String) */ public void clearCache() { Map clearedClasses = classCache.clearAll(); sourceCache.clear(); for (Map.Entry entry : clearedClasses.entrySet()) { // Another Thread may be using an instance of this class // (for the first time) requiring a ClassInfo lock and // classloading which would require a lock on classCache. // The following locks on ClassInfo and to avoid deadlock // should not be done with a classCache lock. InvokerHelper.removeClass(entry.getValue()); } } /** * Closes this GroovyClassLoader and clears any caches it maintains. *

* No use should be made of this instance after this method is * invoked. Any classes that are already loaded are still accessible. * * @throws IOException * @see URLClassLoader#close() * @see #clearCache() * @since 2.5.0 */ @Override public void close() throws IOException { super.close(); clearCache(); } private static class TimestampAdder implements CompilationUnit.IPrimaryClassNodeOperation, Opcodes { private static final TimestampAdder INSTANCE = new TimestampAdder(); private TimestampAdder() {} protected void addTimeStamp(ClassNode node) { if (node.getDeclaredField(Verifier.__TIMESTAMP) == null) { // in case if verifier visited the call already FieldNode timeTagField = new FieldNode( Verifier.__TIMESTAMP, ACC_PUBLIC | ACC_STATIC | ACC_SYNTHETIC, ClassHelper.long_TYPE, //"", node, new ConstantExpression(System.currentTimeMillis())); // alternatively, FieldNode timeTagField = SourceUnit.createFieldNode("public static final long __timeStamp = " + System.currentTimeMillis() + "L"); timeTagField.setSynthetic(true); node.addField(timeTagField); timeTagField = new FieldNode( Verifier.__TIMESTAMP__ + System.currentTimeMillis(), ACC_PUBLIC | ACC_STATIC | ACC_SYNTHETIC, ClassHelper.long_TYPE, //"", node, new ConstantExpression((long) 0)); // alternatively, FieldNode timeTagField = SourceUnit.createFieldNode("public static final long __timeStamp = " + System.currentTimeMillis() + "L"); timeTagField.setSynthetic(true); node.addField(timeTagField); } } @Override public void call(final SourceUnit source, final GeneratorContext context, final ClassNode classNode) throws CompilationFailedException { if ((classNode.getModifiers() & Opcodes.ACC_INTERFACE) > 0) { // does not apply on interfaces return; } if (!(classNode instanceof InnerClassNode)) { addTimeStamp(classNode); } } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy