http://git-wip-us.apache.org/repos/asf/groovy/blob/d638ca43/src/main/groovy/groovy/lang/GrabExclude.java ---------------------------------------------------------------------- diff --git a/src/main/groovy/groovy/lang/GrabExclude.java b/src/main/groovy/groovy/lang/GrabExclude.java new file mode 100644 index 0000000..eb5a50d --- /dev/null +++ b/src/main/groovy/groovy/lang/GrabExclude.java @@ -0,0 +1,61 @@ +/* + * 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 groovy.lang; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Used to exclude an indirectly referenced artifact (a transitive dependency) from the classpath. + * <p> + * Examples:<br> + * {@code @GrabExclude(group='mysql', module='mysql-connector-java') // group/module form}<br> + * {@code @GrabExclude('mysql:mysql-connector-java') // compact form}<br> + * <p> + * Further information about customising grape behavior can be found on the Grape documentation page: + * <a href="http://groovy-lang.org/grape.html">http://groovy-lang.org/grape.html</a>. + */ +@Retention(RetentionPolicy.SOURCE) +@Target({ + ElementType.CONSTRUCTOR, + ElementType.FIELD, + ElementType.LOCAL_VARIABLE, + ElementType.METHOD, + ElementType.PARAMETER, + ElementType.TYPE}) +public @interface GrabExclude { + + /** + * The organisation or group, e.g.: "org.apache.ant"; required unless the compact form is used. + */ + String group() default ""; + + /** + * The module or artifact, e.g.: "ant-junit"; required unless the compact form is used. + */ + String module() default ""; + + /** + * Allows you to specify the group (organisation) and the module (artifact) in one of two compact convenience formats, + * e.g.: <code>@GrabExclude('org.apache.ant:ant-junit')</code> or <code>@GrabExclude('org.apache.ant#ant-junit')</code> + */ + String value() default ""; +} \ No newline at end of file
http://git-wip-us.apache.org/repos/asf/groovy/blob/d638ca43/src/main/groovy/groovy/lang/GrabResolver.java ---------------------------------------------------------------------- diff --git a/src/main/groovy/groovy/lang/GrabResolver.java b/src/main/groovy/groovy/lang/GrabResolver.java new file mode 100644 index 0000000..cf8d0ac --- /dev/null +++ b/src/main/groovy/groovy/lang/GrabResolver.java @@ -0,0 +1,86 @@ +/* + * 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 groovy.lang; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Used to add a repository for resolving Grape dependencies. + * <p> + * For example: + * <pre> + * {@code @GrabResolver}(name='restlet.org', root='http://maven.restlet.org') + * {@code @Grab}(group='org.restlet', module='org.restlet', version='1.1.6') + * class MyRestlet extends org.restlet.Restlet { + * // ... + * } + * </pre> + * By default, the Grapes subsystem uses an Ivy chained resolver. Each resolver + * added using {@code @GrabResolver} is appended to the chain. By default, the grape + * subsystem is shared globally, so added resolvers will become available for any subsequent + * grab calls. Dependency resolution follows Ivy's artifact resolution which tries + * to resolve artifacts in the order specified in the chain of resolvers. + * <p> + * Further information about customising grape behavior can be found on the Grape documentation page: + * <a href="http://groovy-lang.org/grape.html">http://groovy-lang.org/grape.html</a>. + * + * @author Merlyn Albery-Speyer + */ +@Retention(RetentionPolicy.SOURCE) +@Target({ + ElementType.CONSTRUCTOR, + ElementType.FIELD, + ElementType.LOCAL_VARIABLE, + ElementType.METHOD, + ElementType.PARAMETER, + ElementType.TYPE}) +public @interface GrabResolver { + /** + * Allows a shorthand form which sets the name and root to this value. + * Must not be used if name() or root() is non-empty. + */ + String value() default ""; + + /** + * A meaningful name for a repo containing the grape/artifact. + * A non-empty value is required unless value() is used. + */ + String name() default ""; + + /** + * The URL for a repo containing the grape/artifact. + * A non-empty value is required unless value() is used. + */ + String root() default ""; + + /** + * Defaults to Maven2 compatibility. Set false for Ivy only compatibility. + */ + boolean m2Compatible() default true; + + /** + * By default, when a {@code @GrabResolver} annotation is used, a {@code Grape.addResolver()} call is added + * to the static initializers of the class the annotatable node appears in. + * If you wish to disable this, add {@code initClass=false} to the annotation. + */ + boolean initClass() default true; +} http://git-wip-us.apache.org/repos/asf/groovy/blob/d638ca43/src/main/groovy/groovy/lang/Grapes.java ---------------------------------------------------------------------- diff --git a/src/main/groovy/groovy/lang/Grapes.java b/src/main/groovy/groovy/lang/Grapes.java new file mode 100644 index 0000000..8719c74 --- /dev/null +++ b/src/main/groovy/groovy/lang/Grapes.java @@ -0,0 +1,71 @@ +/* + * 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 groovy.lang; + +/** + * Sometimes we will need more than one grab per class, but we can only add + * one annotation type per annotatable node. This class allows for multiple + * grabs to be added. + * <p> + * For example: + * <p> + * <pre> + * {@code @Grapes([@Grab(module='m1'), @Grab(module='m2')])} + * class AnnotatedClass { ... } + * </pre> + * <p> + * You can override an implicit transitive dependency by providing an explicit one. + * E.g. htmlunit 2.6 normally uses xerces 2.9.1 but you can get 2.9.0 as follows: + * <pre> + * {@code @Grapes}([ + * {@code @Grab}('net.sourceforge.htmlunit:htmlunit:2.6'), + * {@code @Grab}('xerces#xercesImpl;2.9.0') + * ]) + * </pre> + * Obviously, only do this if you understand the consequences. + * <p> + * You can also remove transitive dependencies altogether (provided you + * know you don't need them) using {@code @GrabExclude}. + * For example, here is how we would not grab the {@code logkit} and + * {@code avalon-framework} transitive dependencies for Apache POI: + * <pre> + * {@code @Grapes}([ + * {@code @Grab}("org.apache.poi#poi;3.5-beta6"), + * {@code @GrabExclude}("logkit:logkit"), + * {@code @GrabExclude}("avalon-framework#avalon-framework") + * ]) + * import org.apache.poi.hssf.util.CellReference + * assert new CellReference(0, 0, false, false).formatAsString() == 'A1' + * assert new CellReference(1, 3).formatAsString() == '$D$2' + * </pre> + * It is also sometimes also useful to use {@code @GrabConfig} to further adjust how dependencies + * are grabbed. See {@code @GrabConfig} for further information. + */ +public @interface Grapes { + Grab[] value(); + + /** + * This will be pushed into the child grab annotations if the value is not + * set in the child annotation already. + * <p> + * This results in an effective change in the default value, which each @Grab + * can still override + */ + boolean initClass() default true; +} http://git-wip-us.apache.org/repos/asf/groovy/blob/d638ca43/src/main/groovy/groovy/lang/GroovyCallable.java ---------------------------------------------------------------------- diff --git a/src/main/groovy/groovy/lang/GroovyCallable.java b/src/main/groovy/groovy/lang/GroovyCallable.java new file mode 100644 index 0000000..e425c92 --- /dev/null +++ b/src/main/groovy/groovy/lang/GroovyCallable.java @@ -0,0 +1,32 @@ +/* + * 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 groovy.lang; + +import java.util.concurrent.Callable; + +/** + * A special "marker" style interface allowing Groovy classes to implement both + * Runnable and Callable yet give preference to Runnable (for backwards compatibility) + * for APIs having both Runnable and Callable methods. You should generally NOT use this + * method in your own code. + * + * @see java.util.concurrent.Callable + * @since 1.8.0 + */ +public interface GroovyCallable<V> extends Callable<V> { } http://git-wip-us.apache.org/repos/asf/groovy/blob/d638ca43/src/main/groovy/groovy/lang/GroovyClassLoader.java ---------------------------------------------------------------------- diff --git a/src/main/groovy/groovy/lang/GroovyClassLoader.java b/src/main/groovy/groovy/lang/GroovyClassLoader.java new file mode 100644 index 0000000..8581879 --- /dev/null +++ b/src/main/groovy/groovy/lang/GroovyClassLoader.java @@ -0,0 +1,1099 @@ +/* + * 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.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.IOGroovyMethods; +import org.codehaus.groovy.runtime.InvokerHelper; +import org.codehaus.groovy.runtime.memoize.ConcurrentCommonCache; +import org.codehaus.groovy.runtime.memoize.EvictableCache; +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.AccessController; +import java.security.CodeSource; +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. + * + * @author <a href="mailto:[email protected]">James Strachan</a> + * @author Guillaume Laforge + * @author Steve Goetze + * @author Bing Ran + * @author <a href="mailto:[email protected]">Scott Stirling</a> + * @author <a href="mailto:[email protected]">Jochen Theodorou</a> + */ +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<String, Class> classCache = new ConcurrentCommonCache<String, Class>(); + + /** + * This cache contains mappings of file name to class. It is used + * to bypass compilation. + */ + protected final ConcurrentCommonCache<String, Class> sourceCache = new ConcurrentCommonCache<String, Class>(); + + private final CompilerConfiguration config; + private String sourceEncoding; + 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); + } + } + + 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. + * <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"; + } + + public Class parseClass(final Reader reader, final String fileName) throws CompilationFailedException { + GroovyCodeSource gcs = AccessController.doPrivileged(new PrivilegedAction<GroovyCodeSource>() { + public GroovyCodeSource run() { + 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); + } + + /** + * @deprecated Prefer using methods taking a Reader rather than an InputStream to avoid wrong encoding issues. + * Use {@link #parseClass(Reader, String) parseClass} instead + */ + @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(final GroovyCodeSource codeSource, boolean shouldCacheSource) throws CompilationFailedException { + return sourceCache.getAndPut( + codeSource.getName(), + new EvictableCache.ValueProvider<String, Class>() { + @Override + public Class provide(String key) { + return doParseClass(codeSource); + } + }, + shouldCacheSource + ); + } + + 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) { + 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 + */ + 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)) { + 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(source.openStream(), 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) + */ + 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() { + return classCache.values().toArray(EMPTY_CLASS_ARRAY); + } + + /** + * Removes all classes from the class cache. + * <p> + * 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<String, Class> clearedClasses = classCache.clear(); + + sourceCache.clear(); + + for (Map.Entry<String, Class> 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. + * <p> + * 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 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); + } + } + } +} http://git-wip-us.apache.org/repos/asf/groovy/blob/d638ca43/src/main/groovy/groovy/lang/GroovyCodeSource.java ---------------------------------------------------------------------- diff --git a/src/main/groovy/groovy/lang/GroovyCodeSource.java b/src/main/groovy/groovy/lang/GroovyCodeSource.java new file mode 100644 index 0000000..a5a2bb1 --- /dev/null +++ b/src/main/groovy/groovy/lang/GroovyCodeSource.java @@ -0,0 +1,266 @@ +/* + * 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 groovy.lang; + +import groovy.security.GroovyCodeSourcePermission; +import groovy.util.CharsetToolkit; +import org.codehaus.groovy.runtime.IOGroovyMethods; +import org.codehaus.groovy.runtime.ResourceGroovyMethods; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.Reader; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URL; +import java.net.URLConnection; +import java.security.AccessController; +import java.security.CodeSource; +import java.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; +import java.security.cert.Certificate; +import java.util.Objects; + +/** + * CodeSource wrapper class that allows specific security policies to be associated with a class + * compiled from groovy source. + * + * @author Steve Goetze + * @author Guillaume Laforge + * @author Merlyn Albery-Speyer + */ +public class GroovyCodeSource { + + /** + * The codeSource to be given the generated class. This can be used by policy file + * grants to administer security. + */ + private CodeSource codeSource; + + /** + * The name given to the generated class + */ + private String name; + + /** + * The groovy source to be compiled and turned into a class + */ + private String scriptText; + + /** + * The certificates used to sign the items from the codesource + */ + Certificate[] certs; + + private boolean cachable; + + private File file; + + private URL url; + + public GroovyCodeSource(String script, String name, String codeBase) { + this.name = name; + this.scriptText = script; + this.codeSource = createCodeSource(codeBase); + this.cachable = true; + } + + /** + * Construct a GroovyCodeSource for an inputStream of groovyCode that has an + * unknown provenance -- meaning it didn't come from a File or a URL (e.g. a String). + * The supplied codeBase will be used to construct a File URL that should match up + * with a java Policy entry that determines the grants to be associated with the + * class that will be built from the InputStream. + * <p> + * The permission groovy.security.GroovyCodeSourcePermission will be used to determine if the given codeBase + * may be specified. That is, the current Policy set must have a GroovyCodeSourcePermission that implies + * the codeBase, or an exception will be thrown. This is to prevent callers from hijacking + * existing codeBase policy entries unless explicitly authorized by the user. + */ + public GroovyCodeSource(Reader reader, String name, String codeBase) { + this.name = name; + this.codeSource = createCodeSource(codeBase); + + try { + this.scriptText = IOGroovyMethods.getText(reader); + } catch (IOException e) { + throw new RuntimeException("Impossible to read the text content from that reader, for script: " + name + " with codeBase: " + codeBase, e); + } + } + + public GroovyCodeSource(final File infile, final String encoding) throws IOException { + // avoid files which confuse us like ones with .. in path + final File file = new File(infile.getCanonicalPath()); + if (!file.exists()) { + throw new FileNotFoundException(file.toString() + " (" + file.getAbsolutePath() + ")"); + } + if (file.isDirectory()) { + throw new IllegalArgumentException(file.toString() + " (" + file.getAbsolutePath() + ") is a directory not a Groovy source file."); + } + try { + if (!file.canRead()) + throw new RuntimeException(file.toString() + " can not be read. Check the read permission of the file \"" + file.toString() + "\" (" + file.getAbsolutePath() + ")."); + } + catch (SecurityException e) { + throw e; + } + + this.file = file; + this.cachable = true; + //The calls below require access to user.dir - allow here since getName() and getCodeSource() are + //package private and used only by the GroovyClassLoader. + try { + Object[] info = AccessController.doPrivileged(new PrivilegedExceptionAction<Object[]>() { + public Object[] run() throws IOException { + // retrieve the content of the file using the provided encoding + if (encoding != null) { + scriptText = ResourceGroovyMethods.getText(infile, encoding); + } else { + scriptText = ResourceGroovyMethods.getText(infile); + } + + Object[] info = new Object[2]; + URL url = file.toURI().toURL(); + info[0] = url.toExternalForm(); + //toURI().toURL() will encode, but toURL() will not. + info[1] = new CodeSource(url, (Certificate[]) null); + return info; + } + }); + + this.name = (String) info[0]; + this.codeSource = (CodeSource) info[1]; + } catch (PrivilegedActionException pae) { + Throwable cause = pae.getCause(); + if (cause != null && cause instanceof IOException) { + throw (IOException) cause; + } + throw new RuntimeException("Could not construct CodeSource for file: " + file, cause); + } + } + + /** + * @param infile the file to create a GroovyCodeSource for. + * @throws IOException if an issue arises opening and reading the file. + */ + public GroovyCodeSource(final File infile) throws IOException { + this(infile, CharsetToolkit.getDefaultSystemCharset().name()); + } + + public GroovyCodeSource(URI uri) throws IOException { + this(uri.toURL()); + } + + public GroovyCodeSource(URL url) { + if (url == null) { + throw new RuntimeException("Could not construct a GroovyCodeSource from a null URL"); + } + this.url = url; + // TODO: GROOVY-6561: GroovyMain got the name this way: script.substring(script.lastIndexOf("/") + 1) + this.name = url.toExternalForm(); + this.codeSource = new CodeSource(url, (java.security.cert.Certificate[]) null); + try { + String contentEncoding = getContentEncoding(url); + if (contentEncoding != null) { + this.scriptText = ResourceGroovyMethods.getText(url, contentEncoding); + } else { + this.scriptText = ResourceGroovyMethods.getText(url); // falls-back on default encoding + } + } catch (IOException e) { + throw new RuntimeException("Impossible to read the text content from " + name, e); + } + } + + /** + * TODO(jwagenleitner): remove or fix in future release + * + * According to the spec getContentEncoding() returns the Content-Encoding + * HTTP Header which typically carries values such as 'gzip' or 'deflate' + * and is not the character set encoding. For compatibility in 2.4.x, + * this behavior is retained but should be removed or fixed (parse + * charset from Content-Type header) in future releases. + * + * see GROOVY-8056 and https://github.com/apache/groovy/pull/500 + */ + private static String getContentEncoding(URL url) throws IOException { + URLConnection urlConnection = url.openConnection(); + String encoding = urlConnection.getContentEncoding(); + try { + IOGroovyMethods.closeQuietly(urlConnection.getInputStream()); + } catch (IOException ignore) { + // For compatibility, ignore exceptions from getInputStream() call + } + return encoding; + } + + CodeSource getCodeSource() { + return codeSource; + } + + public String getScriptText() { + return scriptText; + } + + public String getName() { + return name; + } + + public File getFile() { + return file; + } + + public URL getURL() { + return url; + } + + public void setCachable(boolean b) { + cachable = b; + } + + public boolean isCachable() { + return cachable; + } + + private static CodeSource createCodeSource(final String codeBase) { + SecurityManager sm = System.getSecurityManager(); + if (sm != null) { + sm.checkPermission(new GroovyCodeSourcePermission(codeBase)); + } + try { + return new CodeSource(new URL("file", "", codeBase), (java.security.cert.Certificate[]) null); + } + catch (MalformedURLException e) { + throw new RuntimeException("A CodeSource file URL cannot be constructed from the supplied codeBase: " + codeBase); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + GroovyCodeSource that = (GroovyCodeSource) o; + return Objects.equals(codeSource, that.codeSource); + } + + @Override + public int hashCode() { + return Objects.hash(codeSource); + } +} http://git-wip-us.apache.org/repos/asf/groovy/blob/d638ca43/src/main/groovy/groovy/lang/GroovyInterceptable.java ---------------------------------------------------------------------- diff --git a/src/main/groovy/groovy/lang/GroovyInterceptable.java b/src/main/groovy/groovy/lang/GroovyInterceptable.java new file mode 100644 index 0000000..8f83354 --- /dev/null +++ b/src/main/groovy/groovy/lang/GroovyInterceptable.java @@ -0,0 +1,28 @@ +/* + * 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 groovy.lang; + +/** + * Marker interface used to notify that all methods should be intercepted through the <code>invokeMethod</code> mechanism + * of <code>GroovyObject</code>. + * + * @author Guillaume Laforge + */ +public interface GroovyInterceptable extends GroovyObject { +} http://git-wip-us.apache.org/repos/asf/groovy/blob/d638ca43/src/main/groovy/groovy/lang/GroovyObject.java ---------------------------------------------------------------------- diff --git a/src/main/groovy/groovy/lang/GroovyObject.java b/src/main/groovy/groovy/lang/GroovyObject.java new file mode 100644 index 0000000..5f271e3 --- /dev/null +++ b/src/main/groovy/groovy/lang/GroovyObject.java @@ -0,0 +1,68 @@ +/* + * 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 groovy.lang; + +/** + * The interface implemented by all Groovy objects. + * <p> + * Especially handy for using Groovy objects when in the Java world. + * + * @author <a href="mailto:[email protected]">James Strachan</a> + */ +public interface GroovyObject { + + /** + * Invokes the given method. + * + * @param name the name of the method to call + * @param args the arguments to use for the method call + * @return the result of invoking the method + */ + Object invokeMethod(String name, Object args); + + /** + * Retrieves a property value. + * + * @param propertyName the name of the property of interest + * @return the given property + */ + Object getProperty(String propertyName); + + /** + * Sets the given property to the new value. + * + * @param propertyName the name of the property of interest + * @param newValue the new value for the property + */ + void setProperty(String propertyName, Object newValue); + + /** + * Returns the metaclass for a given class. + * + * @return the metaClass of this instance + */ + MetaClass getMetaClass(); + + /** + * Allows the MetaClass to be replaced with a derived implementation. + * + * @param metaClass the new metaclass + */ + void setMetaClass(MetaClass metaClass); +} http://git-wip-us.apache.org/repos/asf/groovy/blob/d638ca43/src/main/groovy/groovy/lang/GroovyObjectSupport.java ---------------------------------------------------------------------- diff --git a/src/main/groovy/groovy/lang/GroovyObjectSupport.java b/src/main/groovy/groovy/lang/GroovyObjectSupport.java new file mode 100644 index 0000000..cf7a77f --- /dev/null +++ b/src/main/groovy/groovy/lang/GroovyObjectSupport.java @@ -0,0 +1,63 @@ +/* + * 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 groovy.lang; + +import org.codehaus.groovy.runtime.InvokerHelper; + +/** + * A useful base class for Java objects wishing to be Groovy objects + * + * @author <a href="mailto:[email protected]">James Strachan</a> + */ +public abstract class GroovyObjectSupport implements GroovyObject { + + // never persist the MetaClass + private transient MetaClass metaClass; + + public GroovyObjectSupport() { + this.metaClass = getDefaultMetaClass(); + } + + public Object getProperty(String property) { + return getMetaClass().getProperty(this, property); + } + + public void setProperty(String property, Object newValue) { + getMetaClass().setProperty(this, property, newValue); + } + + public Object invokeMethod(String name, Object args) { + return getMetaClass().invokeMethod(this, name, args); + } + + public MetaClass getMetaClass() { + return this.metaClass; + } + + public void setMetaClass(MetaClass metaClass) { + this.metaClass = + null == metaClass + ? getDefaultMetaClass() + : metaClass; + } + + private MetaClass getDefaultMetaClass() { + return InvokerHelper.getMetaClass(this.getClass()); + } +} http://git-wip-us.apache.org/repos/asf/groovy/blob/d638ca43/src/main/groovy/groovy/lang/GroovyResourceLoader.java ---------------------------------------------------------------------- diff --git a/src/main/groovy/groovy/lang/GroovyResourceLoader.java b/src/main/groovy/groovy/lang/GroovyResourceLoader.java new file mode 100644 index 0000000..7abe202 --- /dev/null +++ b/src/main/groovy/groovy/lang/GroovyResourceLoader.java @@ -0,0 +1,39 @@ +/* + * 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 groovy.lang; + +import java.net.MalformedURLException; +import java.net.URL; + +/** + * Allows frameworks that integrate with Groovy to determine how Groovy files are resolved. + * + * @author Steven Devijver + */ +public interface GroovyResourceLoader { + + /** + * Loads a Groovy source file given its name. + * + * @param filename name of the file + * @return a URL + * @throws java.net.MalformedURLException if the URL is invalid + */ + URL loadGroovySource(String filename) throws MalformedURLException; +} http://git-wip-us.apache.org/repos/asf/groovy/blob/d638ca43/src/main/groovy/groovy/lang/GroovyRuntimeException.java ---------------------------------------------------------------------- diff --git a/src/main/groovy/groovy/lang/GroovyRuntimeException.java b/src/main/groovy/groovy/lang/GroovyRuntimeException.java new file mode 100644 index 0000000..33eb51c --- /dev/null +++ b/src/main/groovy/groovy/lang/GroovyRuntimeException.java @@ -0,0 +1,88 @@ +/* + * 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 groovy.lang; + +import org.codehaus.groovy.ast.ASTNode; +import org.codehaus.groovy.ast.ModuleNode; + +/** + * An exception thrown by the interpreter + * + * @author <a href="mailto:[email protected]">James Strachan</a> + */ +public class GroovyRuntimeException extends RuntimeException { + + private ModuleNode module; + private ASTNode node; + + public GroovyRuntimeException() { + } + + public GroovyRuntimeException(String message) { + super(message); + } + + public GroovyRuntimeException(String message, ASTNode node) { + super(message); + this.node = node; + } + + public GroovyRuntimeException(String message, Throwable cause) { + super(message, cause); + } + + public GroovyRuntimeException(Throwable t) { + super(); + initCause(t); + } + + public void setModule(ModuleNode module) { + this.module = module; + } + + public ModuleNode getModule() { + return module; + } + + public String getMessage() { + return getMessageWithoutLocationText() + getLocationText(); + } + + public ASTNode getNode() { + return node; + } + + public String getMessageWithoutLocationText() { + return super.getMessage(); + } + + protected String getLocationText() { + String answer = ". "; + if (node != null) { + answer += "At [" + node.getLineNumber() + ":" + node.getColumnNumber() + "] "; + } + if (module != null) { + answer += module.getDescription(); + } + if (answer.equals(". ")) { + return ""; + } + return answer; + } +}
