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

org.codehaus.groovy.control.ClassNodeResolver 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.
 */
package org.codehaus.groovy.control;

import groovy.lang.GroovyClassLoader;
import org.codehaus.groovy.GroovyBugError;
import org.codehaus.groovy.ast.ClassHelper;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.ast.decompiled.AsmDecompiler;
import org.codehaus.groovy.ast.decompiled.AsmReferenceResolver;
import org.codehaus.groovy.ast.decompiled.DecompiledClassNode;
import org.codehaus.groovy.classgen.Verifier;
import org.objectweb.asm.Opcodes;

import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.util.HashMap;
import java.util.Map;

/**
 * This class is used as a pluggable way to resolve class names.
 * An instance of this class has to be added to {@link CompilationUnit} using
 * {@link CompilationUnit#setClassNodeResolver(ClassNodeResolver)}. The
 * CompilationUnit will then set the resolver on the {@link ResolveVisitor} each
 * time new. The ResolveVisitor will prepare name lookup and then finally ask
 * the resolver if the class exists. This resolver then can return either a
 * SourceUnit or a ClassNode. In case of a SourceUnit the compiler is notified
 * that a new source is to be added to the compilation queue. In case of a
 * ClassNode no further action than the resolving is done. The lookup result
 * is stored in the helper class {@link LookupResult}. This class provides a
 * class cache to cache lookups. If you don't want this, you have to override
 * the methods {@link ClassNodeResolver#cacheClass(String, ClassNode)} and
 * {@link ClassNodeResolver#getFromClassCache(String)}. Custom lookup logic is
 * supposed to go into the method
 * {@link ClassNodeResolver#findClassNode(String, CompilationUnit)} while the
 * entry method is {@link ClassNodeResolver#resolveName(String, CompilationUnit)}
 */
public class ClassNodeResolver {

    /**
     * Helper class to return either a SourceUnit or ClassNode.
     */
    public static class LookupResult {
        private final SourceUnit su;
        private final ClassNode cn;
        /**
         * creates a new LookupResult. You are not supposed to supply
         * a SourceUnit and a ClassNode at the same time
         */
        public LookupResult(SourceUnit su, ClassNode cn) {
            this.su = su;
            this.cn = cn;
            if (su==null && cn==null) throw new IllegalArgumentException("Either the SourceUnit or the ClassNode must not be null.");
            if (su!=null && cn!=null) throw new IllegalArgumentException("SourceUnit and ClassNode cannot be set at the same time.");
        }
        /**
         * returns true if a ClassNode is stored
         */
        public boolean isClassNode() { return cn!=null; }
        /**
         * returns true if a SourecUnit is stored
         */
        public boolean isSourceUnit() { return su!=null; }
        /**
         * returns the SourceUnit
         */
        public SourceUnit getSourceUnit() { return su; }
        /**
         * returns the ClassNode
         */
        public ClassNode getClassNode() { return cn; }
    }

    // Map to store cached classes
    private final Map cachedClasses = new HashMap<>();
    /**
     * Internal helper used to indicate a cache hit for a class that does not exist.
     * This way further lookups through a slow {@link #findClassNode(String, CompilationUnit)}
     * path can be avoided.
     * WARNING: This class is not to be used outside of ClassNodeResolver.
     */
    protected static final ClassNode NO_CLASS = new ClassNode("NO_CLASS", Opcodes.ACC_PUBLIC,ClassHelper.OBJECT_TYPE) {
        @Override
        public void setRedirect(ClassNode cn) {
            throw new GroovyBugError("This is a dummy class node only! Never use it for real classes.");
        }
    };

    /**
     * Resolves the name of a class to a SourceUnit or ClassNode. If no
     * class or source is found this method returns null. A lookup is done
     * by first asking the cache if there is an entry for the class already available
     * to then call {@link #findClassNode(String, CompilationUnit)}. The result
     * of that method call will be cached if a ClassNode is found. If a SourceUnit
     * is found, this method will not be asked later on again for that class, because
     * ResolveVisitor will first ask the CompilationUnit for classes in the
     * compilation queue and it will find the class for that SourceUnit there then.
     * method return a ClassNode instead of a SourceUnit, the res
     * @param name - the name of the class
     * @param compilationUnit - the current CompilationUnit
     * @return the LookupResult
     */
    public LookupResult resolveName(final String name, final CompilationUnit compilationUnit) {
        ClassNode type = getFromClassCache(name);
        if (type != null) {
            if (type == NO_CLASS) return null;
            return new LookupResult(null, type);
        }

        LookupResult result = findClassNode(name, compilationUnit);
        if (result != null) {
            if (result.isClassNode()) {
                cacheClass(name, result.getClassNode());
            }
            return result;
        } else {
            cacheClass(name, NO_CLASS);
            return null;
        }
    }

    /**
     * caches a ClassNode
     * @param name - the name of the class
     * @param res - the ClassNode for that name
     */
    public void cacheClass(final String name, final ClassNode res) {
        cachedClasses.put(name, res);
    }

    /**
     * returns whatever is stored in the class cache for the given name
     * @param name - the name of the class
     * @return the result of the lookup, which may be null
     */
    public ClassNode getFromClassCache(final String name) {
        // We use here the class cache cachedClasses to prevent
        // calls to ClassLoader#loadClass. Disabling this cache will
        // cause a major performance hit.
        ClassNode cached = cachedClasses.get(name);
        return cached;
    }

    /**
     * Extension point for custom lookup logic of finding ClassNodes. Per default
     * this will use the CompilationUnit class loader to do a lookup on the class
     * path and load the needed class using that loader. Or if a script is found
     * and that script is seen as "newer", the script will be used instead of the
     * class.
     *
     * @param name - the name of the class
     * @param compilationUnit - the current compilation unit
     * @return the lookup result
     */
    public LookupResult findClassNode(final String name, final CompilationUnit compilationUnit) {
        return compilationUnit == null ? null : tryAsLoaderClassOrScript(name, compilationUnit);
    }

    /**
     * This method is used to realize the lookup of a class using the compilation
     * unit class loader. Should no class be found we fall back to a script lookup.
     * If a class is found we check if there is also a script and maybe use that
     * one in case it is newer.

* * Two class search strategies are possible: by ASM decompilation or by usual Java classloading. * The latter is slower but is unavoidable for scripts executed in dynamic environments where * the referenced classes might only be available in the classloader, not on disk. */ private LookupResult tryAsLoaderClassOrScript(final String name, final CompilationUnit compilationUnit) { GroovyClassLoader loader = compilationUnit.getClassLoader(); Map options = compilationUnit.configuration.getOptimizationOptions(); if (!Boolean.FALSE.equals(options.get("asmResolving"))) { LookupResult result = findDecompiled(name, compilationUnit, loader); if (result != null) { return result; } } if (!Boolean.FALSE.equals(options.get("classLoaderResolving"))) { return findByClassLoading(name, compilationUnit, loader); } return tryAsScript(name, compilationUnit, null); } /** * Search for classes using class loading */ private static LookupResult findByClassLoading(final String name, final CompilationUnit compilationUnit, final GroovyClassLoader loader) { Class cls; try { // NOTE: it's important to do no lookup against script files // here since the GroovyClassLoader would create a new CompilationUnit cls = loader.loadClass(name, false, true); } catch (ClassNotFoundException cnfe) { return tryAsScript(name, compilationUnit, null); } catch (CompilationFailedException cfe) { throw new GroovyBugError("The lookup for " + name + " caused a failed compilation. There should not have been any compilation from this call.", cfe); } //TODO: The case of a NoClassDefFoundError needs a bit more research; // a simple recompilation is not possible it seems. The current class // we are searching for is there, so we should mark that somehow. // Basically the missing class needs to be completely compiled before // we can again search for the current name. /*catch (NoClassDefFoundError ncdfe) { cachedClasses.put(name,SCRIPT); return false; }*/ if (cls == null) return null; //NOTE: we might return false here even if we found a class, // because we want to give a possible script a chance to // recompile. This can only be done if the loader was not // the instance defining the class. ClassNode cn = ClassHelper.make(cls); if (cls.getClassLoader() != loader) { return tryAsScript(name, compilationUnit, cn); } return new LookupResult(null,cn); } /** * Search for classes using ASM decompiler */ private LookupResult findDecompiled(final String name, final CompilationUnit compilationUnit, final GroovyClassLoader loader) { ClassNode node = ClassHelper.make(name); if (node.isResolved()) { return new LookupResult(null, node); } DecompiledClassNode asmClass = null; String fileName = name.replace('.', '/') + ".class"; URL resource = loader.getResource(fileName); if (resource != null) { try { asmClass = new DecompiledClassNode(AsmDecompiler.parseClass(resource), new AsmReferenceResolver(this, compilationUnit)); if (!asmClass.getName().equals(name)) { // this may happen under Windows because getResource is case insensitive under that OS! asmClass = null; } } catch (IOException e) { // fall through and attempt other search strategies } } if (asmClass != null) { if (isFromAnotherClassLoader(loader, fileName)) { return tryAsScript(name, compilationUnit, asmClass); } return new LookupResult(null, asmClass); } return null; } private static boolean isFromAnotherClassLoader(final GroovyClassLoader loader, final String fileName) { ClassLoader parent = loader.getParent(); return parent != null && parent.getResource(fileName) != null; } /** * Tries to find a script using the compilation unit class loader. */ private static LookupResult tryAsScript(final String name, final CompilationUnit compilationUnit, final ClassNode oldClass) { LookupResult lr = null; if (oldClass != null) { lr = new LookupResult(null, oldClass); } if (name.startsWith("java.")) { return lr; } int i = name.indexOf('$'); if (i != -1) { return lr; } // try to find a script from classpath GroovyClassLoader gcl = compilationUnit.getClassLoader(); URL url = null; try { url = gcl.getResourceLoader().loadGroovySource(name); } catch (MalformedURLException e) { // fall through and let the URL be null } if (url != null && (oldClass == null || isSourceNewer(url, oldClass))) { SourceUnit sourceUnit = compilationUnit.addSource(url); lr = new LookupResult(sourceUnit, null); } return lr; } /** * Gets the time stamp of a class. *

* NOTE: copied from GroovyClassLoader */ private static long getTimeStamp(final ClassNode cls) { if (!(cls instanceof DecompiledClassNode)) { return Verifier.getTimestamp(cls.getTypeClass()); } return ((DecompiledClassNode) cls).getCompilationTimeStamp(); } /** * Returns true if the source in URL is newer than the class. *

* NOTE: copied from GroovyClassLoader */ private static boolean isSourceNewer(final URL source, final ClassNode cls) { try { long lastMod; // Special handling for file:// protocol, as getLastModified() often reports // incorrect results (-1) if (source.getProtocol().equals("file")) { // Coerce the file URL to a File 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(); } return lastMod > getTimeStamp(cls); } catch (IOException e) { // if the stream can't be opened, let's keep the old reference return false; } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy