/* * 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)} * * @author <a href="mailto:blackdrag@gmx.org">Jochen "blackdrag" Theodorou</a> */ public class ClassNodeResolver { /** * Helper class to return either a SourceUnit or ClassNode. * @author <a href="mailto:blackdrag@gmx.org">Jochen "blackdrag" Theodorou</a> */ 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<String,ClassNode> 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){ 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(String name, CompilationUnit compilationUnit) { ClassNode res = getFromClassCache(name); if (res==NO_CLASS) return null; if (res!=null) return new LookupResult(null,res); LookupResult lr = findClassNode(name, compilationUnit); if (lr != null) { if (lr.isClassNode()) cacheClass(name, lr.getClassNode()); return lr; } 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(String name, 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(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(String name, CompilationUnit compilationUnit) { return 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.<p/> * * 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(String name, CompilationUnit compilationUnit) { GroovyClassLoader loader = compilationUnit.getClassLoader(); Map<String, Boolean> options = compilationUnit.configuration.getOptimizationOptions(); boolean useAsm = !Boolean.FALSE.equals(options.get("asmResolving")); boolean useClassLoader = !Boolean.FALSE.equals(options.get("classLoaderResolving")); LookupResult result = useAsm ? findDecompiled(name, compilationUnit, loader) : null; if (result != null) { return result; } if (!useClassLoader) { return tryAsScript(name, compilationUnit, null); } return findByClassLoading(name, compilationUnit, loader); } /** * Search for classes using class loading */ private static LookupResult findByClassLoading(String name, CompilationUnit compilationUnit, 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) { LookupResult lr = tryAsScript(name, compilationUnit, null); return lr; } catch (CompilationFailedException cfe) { throw new GroovyBugError("The lookup for "+name+" caused a failed compilaton. 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(String name, CompilationUnit compilationUnit, 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(GroovyClassLoader loader, String fileName) { ClassLoader parent = loader.getParent(); return parent != null && parent.getResource(fileName) != null; } /** * try to find a script using the compilation unit class loader. */ private static LookupResult tryAsScript(String name, CompilationUnit compilationUnit, ClassNode oldClass) { LookupResult lr = null; if (oldClass!=null) { lr = new LookupResult(null, oldClass); } if (name.startsWith("java.")) return lr; //TODO: don't ignore inner static classes completely if (name.indexOf('$') != -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 su = compilationUnit.addSource(url); return new LookupResult(su,null); } return lr; } /** * get the time stamp of a class * NOTE: copied from GroovyClassLoader */ private static long getTimeStamp(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(URL source, 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; } } }