blob: 36ccc1740dd1d988894499627ba4ee72fb8d06f2 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.commons.scxml2.env.groovy;
import groovy.lang.GroovyClassLoader;
import groovy.lang.GroovyCodeSource;
import groovy.lang.GroovyRuntimeException;
import groovy.lang.Script;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.LinkedHashMap;
import org.codehaus.groovy.control.CompilerConfiguration;
/**
* GroovyExtendableScriptCache is a general purpose and <em>{@link Serializable}</em> Groovy Script cache.
* <p>
* It provides automatic compilation of scripts and caches the resulting class(es) internally, and after de-serialization
* re-compiles the cached scripts automatically.
* </p>
* <p>
* It also provides easy support for (and scoped) script compilation with a specific {@link Script} base class.
* </p>
* <p>
* Internally it uses a non-serializable and thus transient {@link GroovyClassLoader}, {@link CompilerConfiguration} and
* the parent classloader to use.<br>
* To be able to be serializable, the {@link GroovyClassLoader} is automatically (re)created if not defined yet, and for
* the {@link CompilerConfiguration} and parent classloader it uses serializable instances of
* {@link CompilerConfigurationFactory} and {@link ParentClassLoaderFactory} interfaces which either can be configured
* or have defaults otherwise.
* </p>
* <p>
* The underlying {@link GroovyClassLoader} can be accessed through {@link #getGroovyClassLoader()}, which might be needed
* to de-serialize previously defined/created classes and objects through this class, from within a containing object
* readObject(ObjectInputStream in) method.<br>
* For more information how this works and should be done, see:
* <a href="https://issues.apache.org/jira/browse/GROOVY-1627">Groovy-1627: Deserialization fails to work</a>
* </p>
* <p>
* One other optional feature is script pre-processing which can be configured through an instance of the
* {@link ScriptPreProcessor} interface (also {@link Serializable} of course).<br>
* When configured, the script source will be passed through the {@link ScriptPreProcessor#preProcess(String)} method
* before being compiled.
* </p>
* <p>
* The cache itself as well as the underlying GroovyClassLoader caches can be cleared through {@link #clearCache()}.
* </p>
* <p>
* The GroovyExtendableScriptCache has no other external dependencies other than Groovy itself,
* so can be used independent of Commons SCXML.
* </p>
*/
public class GroovyExtendableScriptCache implements Serializable {
private static final long serialVersionUID = 1L;
/**
* Serializable factory interface providing the Groovy parent ClassLoader,
* needed to restore the specific ClassLoader after de-serialization
*/
public interface ParentClassLoaderFactory extends Serializable {
ClassLoader getClassLoader();
}
/**
* Serializable factory interface providing the Groovy CompilerConfiguration,
* needed to restore the specific CompilerConfiguration after de-serialization
*/
public interface CompilerConfigurationFactory extends Serializable {
CompilerConfiguration getCompilerConfiguration();
}
public interface ScriptPreProcessor extends Serializable {
String preProcess(String script);
}
/** Default CodeSource code base for the compiled Groovy scripts */
public static final String DEFAULT_SCRIPT_CODE_BASE = "/groovy/scxml/script";
/** Default factory for the Groovy parent ClassLoader, returning this class its ClassLoader */
public static final ParentClassLoaderFactory DEFAULT_PARENT_CLASS_LOADER_FACTORY =
(ParentClassLoaderFactory) GroovyExtendableScriptCache.class::getClassLoader;
/** Default factory for the Groovy CompilerConfiguration, returning a new and unmodified CompilerConfiguration instance */
public static final CompilerConfigurationFactory DEFAULT_COMPILER_CONFIGURATION_FACTORY =
(CompilerConfigurationFactory) CompilerConfiguration::new;
protected static class ScriptCacheElement implements Serializable {
private static final long serialVersionUID = 1L;
protected final String baseClass;
protected final String scriptSource;
protected String scriptName;
protected transient Class<? extends Script> scriptClass;
public ScriptCacheElement(final String baseClass, final String scriptSource) {
this.baseClass = baseClass;
this.scriptSource = scriptSource;
}
public String getBaseClass() {
return baseClass;
}
public String getScriptSource() {
return scriptSource;
}
public String getScriptName() {
return scriptName;
}
public void setScriptName(final String scriptName) {
this.scriptName = scriptName;
}
public Class<? extends Script> getScriptClass() {
return scriptClass;
}
public void setScriptClass(final Class<? extends Script> scriptClass) {
this.scriptClass = scriptClass;
}
@Override
public boolean equals(final Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
final ScriptCacheElement that = (ScriptCacheElement) o;
return !(baseClass != null ? !baseClass.equals(that.baseClass) : that.baseClass != null) &&
scriptSource.equals(that.scriptSource);
}
@Override
public int hashCode() {
int result = baseClass != null ? baseClass.hashCode() : 0;
result = 31 * result + scriptSource.hashCode();
return result;
}
}
private final LinkedHashMap<ScriptCacheElement, ScriptCacheElement> scriptCache = new LinkedHashMap<>();
private String scriptCodeBase = DEFAULT_SCRIPT_CODE_BASE;
private String scriptBaseClass;
private ParentClassLoaderFactory parentClassLoaderFactory = DEFAULT_PARENT_CLASS_LOADER_FACTORY;
private CompilerConfigurationFactory compilerConfigurationFactory = DEFAULT_COMPILER_CONFIGURATION_FACTORY;
private ScriptPreProcessor scriptPreProcessor;
/* non-serializable thus transient GroovyClassLoader and CompilerConfiguration */
private transient GroovyClassLoader groovyClassLoader;
private transient CompilerConfiguration compilerConfiguration;
public GroovyExtendableScriptCache() {
}
/*
* Hook into the de-serialization process, reloading the transient GroovyClassLoader, CompilerConfiguration and
* re-generate Script classes through {@link #ensureInitializedOrReloaded()}
*/
private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
ensureInitializedOrReloaded();
}
public ClassLoader getGroovyClassLoader() {
return groovyClassLoader;
}
/**
* @param scriptSource The script source, which will optionally be first preprocessed through {@link #preProcessScript(String)}
* using the configured {@link #getScriptPreProcessor}
* @return A new Script instance from a compiled (or cached) Groovy class parsed from the provided
* scriptSource
*/
public Script getScript(final String scriptSource) {
return getScript(null, scriptSource);
}
public Script getScript(final String scriptBaseClass, final String scriptSource) {
Class<? extends Script> scriptClass;
synchronized (scriptCache) {
ensureInitializedOrReloaded();
final ScriptCacheElement cacheKey = new ScriptCacheElement(scriptBaseClass, scriptSource);
final ScriptCacheElement cacheElement = scriptCache.get(cacheKey);
if (cacheElement != null) {
scriptClass = cacheElement.getScriptClass();
}
else {
final String scriptName = generatedScriptName(scriptSource, scriptCache.size());
scriptClass = compileScript(scriptBaseClass, scriptSource, scriptName);
cacheKey.setScriptName(scriptName);
cacheKey.setScriptClass(scriptClass);
scriptCache.put(cacheKey, cacheKey);
}
}
try {
return scriptClass.newInstance();
} catch (final Exception e) {
throw new GroovyRuntimeException("Failed to create Script instance for class: "+ scriptClass + ". Reason: " + e, e);
}
}
protected void ensureInitializedOrReloaded() {
if (groovyClassLoader == null) {
compilerConfiguration = new CompilerConfiguration(getCompilerConfigurationFactory().getCompilerConfiguration());
if (getScriptBaseClass() != null) {
compilerConfiguration.setScriptBaseClass(getScriptBaseClass());
}
groovyClassLoader = AccessController.doPrivileged((PrivilegedAction<GroovyClassLoader>)
() -> new GroovyClassLoader(getParentClassLoaderFactory().getClassLoader(), compilerConfiguration));
if (!scriptCache.isEmpty()) {
// de-serialized: need to re-generate all previously compiled scripts (this can cause a hick-up...):
for (final ScriptCacheElement element : scriptCache.keySet()) {
element.setScriptClass(compileScript(element.getBaseClass(), element.getScriptSource(), element.getScriptName()));
}
}
}
}
@SuppressWarnings("unchecked")
protected Class<Script> compileScript(final String scriptBaseClass, final String scriptSource, final String scriptName) {
final String script = preProcessScript(scriptSource);
final GroovyCodeSource codeSource = AccessController.doPrivileged((PrivilegedAction<GroovyCodeSource>)
() -> new GroovyCodeSource(script, scriptName, getScriptCodeBase()));
final String currentScriptBaseClass = compilerConfiguration.getScriptBaseClass();
try {
if (scriptBaseClass != null) {
compilerConfiguration.setScriptBaseClass(scriptBaseClass);
}
return groovyClassLoader.parseClass(codeSource, false);
}
finally {
compilerConfiguration.setScriptBaseClass(currentScriptBaseClass);
}
}
protected String preProcessScript(final String scriptSource) {
return getScriptPreProcessor() != null ? getScriptPreProcessor().preProcess(scriptSource) : scriptSource;
}
protected String generatedScriptName(final String scriptSource, final int seed) {
return "script"+seed+"_"+Math.abs(scriptSource.hashCode())+".groovy";
}
/** @return The current configured CodeSource code base used for the compilation of the Groovy scripts */
public String getScriptCodeBase() {
return scriptCodeBase;
}
/**
* @param scriptCodeBase The CodeSource code base to be used for the compilation of the Groovy scripts.<br>
* When null, of zero length or not (at least) starting with a '/',
* the {@link #DEFAULT_SCRIPT_CODE_BASE} will be set instead.
*/
@SuppressWarnings("unused")
public void setScriptCodeBase(final String scriptCodeBase) {
if (scriptCodeBase != null && !scriptCodeBase.isEmpty() && scriptCodeBase.charAt(0) == '/') {
this.scriptCodeBase = scriptCodeBase;
}
else {
this.scriptCodeBase = DEFAULT_SCRIPT_CODE_BASE;
}
}
public String getScriptBaseClass() {
return scriptBaseClass;
}
public void setScriptBaseClass(final String scriptBaseClass) {
this.scriptBaseClass = scriptBaseClass;
}
public ParentClassLoaderFactory getParentClassLoaderFactory() {
return parentClassLoaderFactory;
}
@SuppressWarnings("unused")
public void setParentClassLoaderFactory(final ParentClassLoaderFactory parentClassLoaderFactory) {
this.parentClassLoaderFactory = parentClassLoaderFactory != null ? parentClassLoaderFactory : DEFAULT_PARENT_CLASS_LOADER_FACTORY;
}
public CompilerConfigurationFactory getCompilerConfigurationFactory() {
return compilerConfigurationFactory;
}
@SuppressWarnings("unused")
public void setCompilerConfigurationFactory(final CompilerConfigurationFactory compilerConfigurationFactory) {
this.compilerConfigurationFactory = compilerConfigurationFactory != null ? compilerConfigurationFactory : DEFAULT_COMPILER_CONFIGURATION_FACTORY;
}
public ScriptPreProcessor getScriptPreProcessor() {
return scriptPreProcessor;
}
public void setScriptPreProcessor(final ScriptPreProcessor scriptPreProcessor) {
this.scriptPreProcessor = scriptPreProcessor;
}
public boolean isEmpty() {
synchronized (scriptCache) {
return scriptCache.isEmpty();
}
}
public void clearCache() {
synchronized (scriptCache) {
scriptCache.clear();
if (groovyClassLoader != null) {
groovyClassLoader.clearCache();
}
}
}
}