/* * 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 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.*; import org.codehaus.groovy.runtime.InvokerHelper; import org.codehaus.groovy.runtime.IOGroovyMethods; import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.ClassWriter; import org.objectweb.asm.Opcodes; import java.io.*; import java.net.*; import java.security.*; import java.util.*; /** * 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. * * @author <a href="mailto:james@coredevelopers.net">James Strachan</a> * @author Guillaume Laforge * @author Steve Goetze * @author Bing Ran * @author <a href="mailto:scottstirling@rcn.com">Scott Stirling</a> * @author <a href="mailto:blackdrag@gmx.org">Jochen Theodorou</a> */ public class GroovyClassLoader extends URLClassLoader { private static final URL[] EMPTY_URL_ARRAY = new URL[0]; /** * this cache contains the loaded classes or PARSING, if the class is currently parsed */ protected final Map<String, Class> classCache = new HashMap<String, Class>(); /** * This cache contains mappings of file name to class. It is used * to bypass compilation. */ protected final Map<String, Class> sourceCache = new HashMap<String, Class>(); private final CompilerConfiguration config; private Boolean recompile; // use 1000000 as offset to avoid conflicts with names form the GroovyShell private static int scriptNameCounter = 1000000; private GroovyResourceLoader resourceLoader = new GroovyResourceLoader() { public URL loadGroovySource(final String filename) throws MalformedURLException { return AccessController.doPrivileged(new PrivilegedAction<URL>() { public URL run() { 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); } } } /** * 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. * <p> * 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); } } /** * 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 = AccessController.doPrivileged(new PrivilegedAction<GroovyCodeSource>() { public GroovyCodeSource run() { return new GroovyCodeSource(text, fileName, "/groovy/script"); } }); gcs.setCachable(false); return parseClass(gcs); } /** * 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 { return parseClass(text, "script" + System.currentTimeMillis() + Math.abs(text.hashCode()) + ".groovy"); } public synchronized String generateScriptName() { scriptNameCounter++; return "script" + scriptNameCounter + ".groovy"; } /** * @deprecated Prefer using methods taking a Reader rather than an InputStream to avoid wrong encoding issues. */ @Deprecated public Class parseClass(final InputStream in, final String fileName) throws CompilationFailedException { // For generic input streams, provide a catch-all codebase of GroovyScript // Security for these classes can be administered via policy grants with // a codebase of file:groovy.script GroovyCodeSource gcs = AccessController.doPrivileged(new PrivilegedAction<GroovyCodeSource>() { public GroovyCodeSource run() { try { String scriptText = config.getSourceEncoding() != null ? IOGroovyMethods.getText(in, config.getSourceEncoding()) : IOGroovyMethods.getText(in); return new GroovyCodeSource(scriptText, fileName, "/groovy/script"); } catch (IOException e) { throw new RuntimeException("Impossible to read the content of the input stream 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(GroovyCodeSource codeSource, boolean shouldCacheSource) throws CompilationFailedException { synchronized (sourceCache) { Class answer = sourceCache.get(codeSource.getName()); if (answer != null) return answer; answer = doParseClass(codeSource); if (shouldCacheSource) sourceCache.put(codeSource.getName(), answer); return answer; } } 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!"); } } } 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; } 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 = AccessController.doPrivileged(new PrivilegedAction<ProtectionDomain>() { public ProtectionDomain run() { return getClass().getProtectionDomain(); } }); PermissionCollection myPerms = myDomain.getPermissions(); if (myPerms != null) { for (Enumeration<Permission> 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; } 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(); } public void addClasspath(String path) { delegate.addClasspath(path); } public void clearCache() { delegate.clearCache(); } public URL findResource(String name) { return delegate.findResource(name); } public Enumeration findResources(String name) throws IOException { return delegate.findResources(name); } public Class[] getLoadedClasses() { return delegate.getLoadedClasses(); } public URL getResource(String name) { return delegate.getResource(name); } public InputStream getResourceAsStream(String name) { return delegate.getResourceAsStream(name); } public GroovyResourceLoader getResourceLoader() { return delegate.getResourceLoader(); } public URL[] getURLs() { return delegate.getURLs(); } 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); } public Class parseClass(GroovyCodeSource codeSource, boolean shouldCache) throws CompilationFailedException { return delegate.parseClass(codeSource, shouldCache); } public void setResourceLoader(GroovyResourceLoader resourceLoader) { delegate.setResourceLoader(resourceLoader); } public void addURL(URL url) { delegate.addURL(url); } 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) { InnerLoader loader = AccessController.doPrivileged(new PrivilegedAction<InnerLoader>() { public InnerLoader run() { return new InnerLoader(GroovyClassLoader.this); } }); return new ClassCollector(loader, unit, su); } public static class ClassCollector extends CompilationUnit.ClassgenCallback { private Class generatedClass; private final GroovyClassLoader cl; private final SourceUnit su; private final CompilationUnit unit; private final Collection<Class> loadedClasses; protected ClassCollector(InnerLoader cl, CompilationUnit unit, SourceUnit su) { this.cl = cl; this.unit = unit; this.loadedClasses = new ArrayList<Class>(); 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 = (ClassNode) 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); } public void call(ClassVisitor classWriter, ClassNode classNode) { onClassNode((ClassWriter) classWriter, classNode); } public Collection getLoadedClasses() { return this.loadedClasses; } } /** * open up the super class define that takes raw bytes */ public Class defineClass(String name, byte[] b) { return super.defineClass(name, b, 0, b.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) { if (name == null) return null; synchronized (classCache) { 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) { synchronized (classCache) { 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) { synchronized (classCache) { classCache.remove(name); } } /** * adds a URL to the classloader. * * @param url the new classpath element */ 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. * <p> * 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); if (timestamp == Long.MAX_VALUE) return false; return true; } /** * 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 */ 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)) || (oldClass == null)) { synchronized (sourceCache) { String name = source.toExternalForm(); sourceCache.remove(name); if (isFile(source)) { try { return parseClass(new GroovyCodeSource(new File(source.toURI()), config.getSourceEncoding())); } catch (URISyntaxException e) { // do nothing and fall back to the other version } } return parseClass(source.openStream(), 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) */ 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. * <p> * 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(); if (path.exists() && path.isDirectory()) { File file = new File(path, fileWithoutPackage); if (file.exists()) { // file.exists() might be case insensitive. Let's do // case sensitive match for the filename File parent = file.getParentFile(); for (String child : parent.list()) { if (child.equals(fileWithoutPackage)) return 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) */ public void addClasspath(final String path) { AccessController.doPrivileged(new PrivilegedAction<Void>() { public Void run() { URI newURI; try { newURI = new URI(path); // check if we can create a URL from that URI newURI.toURL(); } catch (URISyntaxException e) { // the URI has a false format, so lets try it with files ... newURI=new File(path).toURI(); } catch (MalformedURLException e) { // the URL has a false format, so lets try it with files ... newURI=new File(path).toURI(); } catch (IllegalArgumentException e) { // the URL is not absolute, 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; } }); } /** * <p>Returns all Groovy classes loaded by this class loader. * * @return all classes loaded by this class loader */ public Class[] getLoadedClasses() { synchronized (classCache) { final Collection<Class> values = classCache.values(); return values.toArray(new Class[values.size()]); } } /** * Removes all classes from the class cache. * * @see #getClassCacheEntry(String) * @see #setClassCacheEntry(Class) * @see #removeClassCacheEntry(String) */ public void clearCache() { synchronized (classCache) { for (Class cl : classCache.values()) { InvokerHelper.removeClass(cl); } classCache.clear(); } synchronized (sourceCache) { sourceCache.clear(); } } @Override public void close() throws IOException { super.close(); clearCache(); } private static class TimestampAdder extends CompilationUnit.PrimaryClassNodeOperation implements 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__ + String.valueOf(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); } } } }