groovy.util.GroovyScriptEngine Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of groovy Show documentation
Show all versions of groovy Show documentation
Groovy: A powerful, dynamic language for the JVM
/*
* 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.util;
import groovy.lang.Binding;
import groovy.lang.GroovyClassLoader;
import groovy.lang.GroovyCodeSource;
import groovy.lang.GroovyResourceLoader;
import groovy.lang.Script;
import org.codehaus.groovy.GroovyBugError;
import org.codehaus.groovy.ast.ClassHelper;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.ast.InnerClassNode;
import org.codehaus.groovy.classgen.GeneratorContext;
import org.codehaus.groovy.control.ClassNodeResolver;
import org.codehaus.groovy.control.CompilationFailedException;
import org.codehaus.groovy.control.CompilationUnit;
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.tools.gse.DependencyTracker;
import org.codehaus.groovy.tools.gse.StringSetMap;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.ref.WeakReference;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.security.AccessController;
import java.security.CodeSource;
import java.security.PrivilegedAction;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
/**
* Specific script engine able to reload modified scripts as well as dealing properly
* with dependent scripts.
*/
public class GroovyScriptEngine implements ResourceConnector {
private static final ClassLoader CL_STUB = AccessController.doPrivileged(new PrivilegedAction() {
public ClassLoader run() {
return new ClassLoader() {};
}
});
private static final URL[] EMPTY_URL_ARRAY = new URL[0];
private static class LocalData {
CompilationUnit cu;
final StringSetMap dependencyCache = new StringSetMap();
final Map precompiledEntries = new HashMap();
}
private static WeakReference> localData = new WeakReference>(null);
private static synchronized ThreadLocal getLocalData() {
ThreadLocal local = localData.get();
if (local != null) return local;
local = new ThreadLocal();
localData = new WeakReference>(local);
return local;
}
private final URL[] roots;
private final ResourceConnector rc;
private final ClassLoader parentLoader;
private GroovyClassLoader groovyLoader;
private final Map scriptCache = new ConcurrentHashMap();
private CompilerConfiguration config;
{
config = new CompilerConfiguration(CompilerConfiguration.DEFAULT);
config.setSourceEncoding(CompilerConfiguration.DEFAULT_SOURCE_ENCODING);
}
//TODO: more finals?
private static class ScriptCacheEntry {
private final Class scriptClass;
private final long lastModified, lastCheck;
private final Set dependencies;
private final boolean sourceNewer;
public ScriptCacheEntry(Class clazz, long modified, long lastCheck, Set depend, boolean sourceNewer) {
this.scriptClass = clazz;
this.lastModified = modified;
this.lastCheck = lastCheck;
this.dependencies = depend;
this.sourceNewer = sourceNewer;
}
public ScriptCacheEntry(ScriptCacheEntry old, long lastCheck, boolean sourceNewer) {
this(old.scriptClass, old.lastModified, lastCheck, old.dependencies, sourceNewer);
}
}
private class ScriptClassLoader extends GroovyClassLoader {
public ScriptClassLoader(GroovyClassLoader loader) {
super(loader);
}
public ScriptClassLoader(ClassLoader loader, CompilerConfiguration config) {
super(loader, config, false);
setResLoader();
}
private void setResLoader() {
final GroovyResourceLoader rl = getResourceLoader();
setResourceLoader(new GroovyResourceLoader() {
public URL loadGroovySource(String className) throws MalformedURLException {
String filename;
for (String extension : getConfig().getScriptExtensions()) {
filename = className.replace('.', File.separatorChar) + "." + extension;
try {
URLConnection dependentScriptConn = rc.getResourceConnection(filename);
return dependentScriptConn.getURL();
} catch (ResourceException e) {
//TODO: maybe do something here?
}
}
return rl.loadGroovySource(className);
}
});
}
@Override
protected CompilationUnit createCompilationUnit(CompilerConfiguration configuration, CodeSource source) {
CompilationUnit cu = super.createCompilationUnit(configuration, source);
LocalData local = getLocalData().get();
local.cu = cu;
final StringSetMap cache = local.dependencyCache;
final Map precompiledEntries = local.precompiledEntries;
// "." is used to transfer compilation dependencies, which will be
// recollected later during compilation
for (String depSourcePath : cache.get(".")) {
try {
cache.get(depSourcePath);
cu.addSource(getResourceConnection(depSourcePath).getURL());
} catch (ResourceException e) {
/* ignore */
}
}
// remove all old entries including the "." entry
cache.clear();
cu.addPhaseOperation(new CompilationUnit.PrimaryClassNodeOperation() {
@Override
public void call(final SourceUnit source, GeneratorContext context, ClassNode classNode)
throws CompilationFailedException {
// GROOVY-4013: If it is an inner class, tracking its dependencies doesn't really
// serve any purpose and also interferes with the caching done to track dependencies
if (classNode instanceof InnerClassNode) return;
DependencyTracker dt = new DependencyTracker(source, cache, precompiledEntries);
dt.visitClass(classNode);
}
}, Phases.CLASS_GENERATION);
cu.setClassNodeResolver(new ClassNodeResolver() {
@Override
public LookupResult findClassNode(String origName, CompilationUnit compilationUnit) {
CompilerConfiguration cc = compilationUnit.getConfiguration();
String name = origName.replace('.', '/');
for (String ext : cc.getScriptExtensions()) {
try {
String finalName = name + "." + ext;
URLConnection conn = rc.getResourceConnection(finalName);
URL url = conn.getURL();
String path = url.toExternalForm();
ScriptCacheEntry entry = scriptCache.get(path);
Class clazz = null;
if (entry != null) clazz = entry.scriptClass;
if (GroovyScriptEngine.this.isSourceNewer(entry)) {
try {
SourceUnit su = compilationUnit.addSource(url);
return new LookupResult(su, null);
} finally {
forceClose(conn);
}
} else {
precompiledEntries.put(origName, path);
}
if (clazz != null) {
ClassNode cn = ClassHelper.make(clazz);
return new LookupResult(null, cn);
}
} catch (ResourceException re) {
// skip
}
}
return super.findClassNode(origName, compilationUnit);
}
});
return cu;
}
@Override
public Class parseClass(GroovyCodeSource codeSource, boolean shouldCacheSource) throws CompilationFailedException {
synchronized (sourceCache) {
return doParseClass(codeSource);
}
}
private Class> doParseClass(GroovyCodeSource codeSource) {
// local is kept as hard reference to avoid garbage collection
ThreadLocal localTh = getLocalData();
LocalData localData = new LocalData();
localTh.set(localData);
StringSetMap cache = localData.dependencyCache;
Class> answer = null;
try {
updateLocalDependencyCache(codeSource, localData);
answer = super.parseClass(codeSource, false);
updateScriptCache(localData);
} finally {
cache.clear();
localTh.remove();
}
return answer;
}
private void updateLocalDependencyCache(GroovyCodeSource codeSource, LocalData localData) {
// we put the old dependencies into local cache so createCompilationUnit
// can pick it up. We put that entry under the name "."
ScriptCacheEntry origEntry = scriptCache.get(codeSource.getName());
Set origDep = null;
if (origEntry != null) origDep = origEntry.dependencies;
if (origDep != null) {
Set newDep = new HashSet(origDep.size());
for (String depName : origDep) {
ScriptCacheEntry dep = scriptCache.get(depName);
try {
if (origEntry == dep || GroovyScriptEngine.this.isSourceNewer(dep)) {
newDep.add(depName);
}
} catch (ResourceException re) {
}
}
StringSetMap cache = localData.dependencyCache;
cache.put(".", newDep);
}
}
private void updateScriptCache(LocalData localData) {
StringSetMap cache = localData.dependencyCache;
cache.makeTransitiveHull();
long time = getCurrentTime();
Set entryNames = new HashSet();
for (Map.Entry> entry : cache.entrySet()) {
String className = entry.getKey();
Class clazz = getClassCacheEntry(className);
if (clazz == null) continue;
String entryName = getPath(clazz, localData.precompiledEntries);
if (entryNames.contains(entryName)) continue;
entryNames.add(entryName);
Set value = convertToPaths(entry.getValue(), localData.precompiledEntries);
long lastModified;
try {
lastModified = getLastModified(entryName);
} catch (ResourceException e) {
lastModified = time;
}
ScriptCacheEntry cacheEntry = new ScriptCacheEntry(clazz, lastModified, time, value, false);
scriptCache.put(entryName, cacheEntry);
}
}
private String getPath(Class clazz, Map precompiledEntries) {
CompilationUnit cu = getLocalData().get().cu;
String name = clazz.getName();
ClassNode classNode = cu.getClassNode(name);
if (classNode == null) {
// this is a precompiled class!
String path = precompiledEntries.get(name);
if (path == null) throw new GroovyBugError("Precompiled class " + name + " should be available in precompiled entries map, but was not.");
return path;
} else {
return classNode.getModule().getContext().getName();
}
}
private Set convertToPaths(Set orig, Map precompiledEntries) {
Set ret = new HashSet();
for (String className : orig) {
Class clazz = getClassCacheEntry(className);
if (clazz == null) continue;
ret.add(getPath(clazz, precompiledEntries));
}
return ret;
}
}
/**
* Simple testing harness for the GSE. Enter script roots as arguments and
* then input script names to run them.
*
* @param urls an array of URLs
* @throws Exception if something goes wrong
*/
public static void main(String[] urls) throws Exception {
GroovyScriptEngine gse = new GroovyScriptEngine(urls);
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
String line;
while (true) {
System.out.print("groovy> ");
if ((line = br.readLine()) == null || line.equals("quit")) {
break;
}
try {
System.out.println(gse.run(line, new Binding()));
} catch (Exception e) {
e.printStackTrace();
}
}
}
/**
* Initialize a new GroovyClassLoader with a default or
* constructor-supplied parentClassLoader.
*
* @return the parent classloader used to load scripts
*/
private GroovyClassLoader initGroovyLoader() {
GroovyClassLoader groovyClassLoader =
AccessController.doPrivileged(new PrivilegedAction() {
public ScriptClassLoader run() {
if (parentLoader instanceof GroovyClassLoader) {
return new ScriptClassLoader((GroovyClassLoader) parentLoader);
} else {
return new ScriptClassLoader(parentLoader, config);
}
}
});
for (URL root : roots) groovyClassLoader.addURL(root);
return groovyClassLoader;
}
/**
* Get a resource connection as a URLConnection
to retrieve a script
* from the ResourceConnector
.
*
* @param resourceName name of the resource to be retrieved
* @return a URLConnection to the resource
* @throws ResourceException
*/
public URLConnection getResourceConnection(String resourceName) throws ResourceException {
// Get the URLConnection
URLConnection groovyScriptConn = null;
ResourceException se = null;
for (URL root : roots) {
URL scriptURL = null;
try {
scriptURL = new URL(root, resourceName);
groovyScriptConn = openConnection(scriptURL);
break; // Now this is a bit unusual
} catch (MalformedURLException e) {
String message = "Malformed URL: " + root + ", " + resourceName;
if (se == null) {
se = new ResourceException(message);
} else {
se = new ResourceException(message, se);
}
} catch (IOException e1) {
String message = "Cannot open URL: " + root + resourceName;
groovyScriptConn = null;
if (se == null) {
se = new ResourceException(message);
} else {
se = new ResourceException(message, se);
}
}
}
if (se == null) se = new ResourceException("No resource for " + resourceName + " was found");
// If we didn't find anything, report on all the exceptions that occurred.
if (groovyScriptConn == null) throw se;
return groovyScriptConn;
}
private static URLConnection openConnection(URL scriptURL) throws IOException {
URLConnection urlConnection = scriptURL.openConnection();
verifyInputStream(urlConnection);
return scriptURL.openConnection();
}
/**
* This method closes a {@link URLConnection} by getting its {@link InputStream} and calling the
* {@link InputStream#close()} method on it. The {@link URLConnection} doesn't have a close() method
* and relies on garbage collection to close the underlying connection to the file.
* Relying on garbage collection could lead to the application exhausting the number of files the
* user is allowed to have open at any one point in time and cause the application to crash
* ({@link java.io.FileNotFoundException} (Too many open files)).
* Hence the need for this method to explicitly close the underlying connection to the file.
*
* @param urlConnection the {@link URLConnection} to be "closed" to close the underlying file descriptors.
*/
private static void forceClose(URLConnection urlConnection) {
if (urlConnection != null) {
// We need to get the input stream and close it to force the open
// file descriptor to be released. Otherwise, we will reach the limit
// for number of files open at one time.
try {
verifyInputStream(urlConnection);
} catch (Exception e) {
// Do nothing: We were not going to use it anyway.
}
}
}
private static void verifyInputStream(URLConnection urlConnection) throws IOException {
try (InputStream in = urlConnection.getInputStream()) {
}
}
/**
* The groovy script engine will run groovy scripts and reload them and
* their dependencies when they are modified. This is useful for embedding
* groovy in other containers like games and application servers.
*
* @param roots This an array of URLs where Groovy scripts will be stored. They should
* be laid out using their package structure like Java classes
*/
private GroovyScriptEngine(URL[] roots, ClassLoader parent, ResourceConnector rc) {
if (roots == null) roots = EMPTY_URL_ARRAY;
this.roots = roots;
if (rc == null) rc = this;
this.rc = rc;
if (parent == CL_STUB) parent = this.getClass().getClassLoader();
this.parentLoader = parent;
this.groovyLoader = initGroovyLoader();
}
public GroovyScriptEngine(URL[] roots) {
this(roots, CL_STUB, null);
}
public GroovyScriptEngine(URL[] roots, ClassLoader parentClassLoader) {
this(roots, parentClassLoader, null);
}
public GroovyScriptEngine(String[] urls) throws IOException {
this(createRoots(urls), CL_STUB, null);
}
private static URL[] createRoots(String[] urls) throws MalformedURLException {
if (urls == null) return null;
URL[] roots = new URL[urls.length];
for (int i = 0; i < roots.length; i++) {
if (urls[i].contains("://")) {
roots[i] = new URL(urls[i]);
} else {
roots[i] = new File(urls[i]).toURI().toURL();
}
}
return roots;
}
public GroovyScriptEngine(String[] urls, ClassLoader parentClassLoader) throws IOException {
this(createRoots(urls), parentClassLoader, null);
}
public GroovyScriptEngine(String url) throws IOException {
this(new String[]{url});
}
public GroovyScriptEngine(String url, ClassLoader parentClassLoader) throws IOException {
this(new String[]{url}, parentClassLoader);
}
public GroovyScriptEngine(ResourceConnector rc) {
this(null, CL_STUB, rc);
}
public GroovyScriptEngine(ResourceConnector rc, ClassLoader parentClassLoader) {
this(null, parentClassLoader, rc);
}
/**
* Get the ClassLoader
that will serve as the parent ClassLoader of the
* {@link GroovyClassLoader} in which scripts will be executed. By default, this is the
* ClassLoader that loaded the GroovyScriptEngine
class.
*
* @return the parent classloader used to load scripts
*/
public ClassLoader getParentClassLoader() {
return parentLoader;
}
/**
* Get the class of the scriptName in question, so that you can instantiate
* Groovy objects with caching and reloading.
*
* @param scriptName resource name pointing to the script
* @return the loaded scriptName as a compiled class
* @throws ResourceException if there is a problem accessing the script
* @throws ScriptException if there is a problem parsing the script
*/
public Class loadScriptByName(String scriptName) throws ResourceException, ScriptException {
URLConnection conn = rc.getResourceConnection(scriptName);
String path = conn.getURL().toExternalForm();
ScriptCacheEntry entry = scriptCache.get(path);
Class clazz = null;
if (entry != null) clazz = entry.scriptClass;
try {
if (isSourceNewer(entry)) {
try {
String encoding = conn.getContentEncoding() != null ? conn.getContentEncoding() : config.getSourceEncoding();
String content = IOGroovyMethods.getText(conn.getInputStream(), encoding);
clazz = groovyLoader.parseClass(content, path);
} catch (IOException e) {
throw new ResourceException(e);
}
}
} finally {
forceClose(conn);
}
return clazz;
}
/**
* Run a script identified by name with a single argument.
*
* @param scriptName name of the script to run
* @param argument a single argument passed as a variable named arg
in the binding
* @return a toString()
representation of the result of the execution of the script
* @throws ResourceException if there is a problem accessing the script
* @throws ScriptException if there is a problem parsing the script
*/
public String run(String scriptName, String argument) throws ResourceException, ScriptException {
Binding binding = new Binding();
binding.setVariable("arg", argument);
Object result = run(scriptName, binding);
return result == null ? "" : result.toString();
}
/**
* Run a script identified by name with a given binding.
*
* @param scriptName name of the script to run
* @param binding the binding to pass to the script
* @return an object
* @throws ResourceException if there is a problem accessing the script
* @throws ScriptException if there is a problem parsing the script
*/
public Object run(String scriptName, Binding binding) throws ResourceException, ScriptException {
return createScript(scriptName, binding).run();
}
/**
* Creates a Script with a given scriptName and binding.
*
* @param scriptName name of the script to run
* @param binding the binding to pass to the script
* @return the script object
* @throws ResourceException if there is a problem accessing the script
* @throws ScriptException if there is a problem parsing the script
*/
public Script createScript(String scriptName, Binding binding) throws ResourceException, ScriptException {
return InvokerHelper.createScript(loadScriptByName(scriptName), binding);
}
private long getLastModified(String scriptName) throws ResourceException {
URLConnection conn = rc.getResourceConnection(scriptName);
long lastMod = 0;
try {
lastMod = conn.getLastModified();
} finally {
// getResourceConnection() opening the inputstream, let's ensure all streams are closed
forceClose(conn);
}
return lastMod;
}
protected boolean isSourceNewer(ScriptCacheEntry entry) throws ResourceException {
if (entry == null) return true;
long mainEntryLastCheck = entry.lastCheck;
long now = 0;
boolean returnValue = false;
for (String scriptName : entry.dependencies) {
ScriptCacheEntry depEntry = scriptCache.get(scriptName);
if (depEntry.sourceNewer) return true;
// check if maybe dependency was recompiled, but this one here not
if (mainEntryLastCheck < depEntry.lastModified) {
returnValue = true;
continue;
}
if (now == 0) now = getCurrentTime();
long nextSourceCheck = depEntry.lastCheck + config.getMinimumRecompilationInterval();
if (nextSourceCheck > now) continue;
long lastMod = getLastModified(scriptName);
if (depEntry.lastModified < lastMod) {
depEntry = new ScriptCacheEntry(depEntry, lastMod, true);
scriptCache.put(scriptName, depEntry);
returnValue = true;
} else {
depEntry = new ScriptCacheEntry(depEntry, now, false);
scriptCache.put(scriptName, depEntry);
}
}
return returnValue;
}
/**
* Returns the GroovyClassLoader associated with this script engine instance.
* Useful if you need to pass the class loader to another library.
*
* @return the GroovyClassLoader
*/
public GroovyClassLoader getGroovyClassLoader() {
return groovyLoader;
}
/**
* @return a non null compiler configuration
*/
public CompilerConfiguration getConfig() {
return config;
}
/**
* sets a compiler configuration
*
* @param config - the compiler configuration
* @throws NullPointerException if config is null
*/
public void setConfig(CompilerConfiguration config) {
if (config == null) throw new NullPointerException("configuration cannot be null");
this.config = config;
this.groovyLoader = initGroovyLoader();
}
protected long getCurrentTime() {
return System.currentTimeMillis();
}
}