001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *     http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.commons.scxml2.env.groovy;
018
019import java.io.IOException;
020import java.io.ObjectInputStream;
021import java.io.Serializable;
022import java.security.AccessController;
023import java.security.PrivilegedAction;
024import java.util.LinkedHashMap;
025
026import org.codehaus.groovy.control.CompilerConfiguration;
027
028import groovy.lang.GroovyClassLoader;
029import groovy.lang.GroovyCodeSource;
030import groovy.lang.GroovyRuntimeException;
031import groovy.lang.Script;
032
033/**
034 * GroovyExtendableScriptCache is a general purpose and <em>{@link Serializable}</em> Groovy Script cache.
035 * <p>
036 * It provides automatic compilation of scripts and caches the resulting class(es) internally, and after de-serialization
037 * re-compiles the cached scripts automatically.
038 * </p>
039 * <p>
040 * It also provides easy support for (and scoped) script compilation with a specific {@link Script} base class.
041 * </p>
042 * <p>
043 * Internally it uses a non-serializable and thus transient {@link GroovyClassLoader}, {@link CompilerConfiguration} and
044 * the parent classloader to use.<br/>
045 * To be able to be serializable, the {@link GroovyClassLoader} is automatically (re)created if not defined yet, and for
046 * the  {@link CompilerConfiguration} and parent classloader it uses serializable instances of
047 * {@link CompilerConfigurationFactory} and {@link ParentClassLoaderFactory} interfaces which either can be configured
048 * or have defaults otherwise.
049 * </p>
050 * <p>
051 * The underlying {@link GroovyClassLoader} can be accessed through {@link #getGroovyClassLoader()}, which might be needed
052 * to de-serialize previously defined/created classes and objects through this class, from within a containing object
053 * readObject(ObjectInputStream in) method.<br/>
054 * For more information how this works and should be done, see:
055 * <a href="http://jira.codehaus.org/browse/GROOVY-1627">Groovy-1627: Deserialization fails to work</a>
056 * </p>
057 * <p>
058 * One other optional feature is script pre-processing which can be configured through an instance of the
059 * {@link ScriptPreProcessor} interface (also {@link Serializable} of course).<br/>
060 * When configured, the script source will be passed through the {@link ScriptPreProcessor#preProcess(String)} method
061 * before being compiled.
062 * </p>
063 * <p>
064 * The cache itself as well as the underlying GroovyClassLoader caches can be cleared through {@link #clearCache()}.
065 * </p>
066 * <p>
067 * The GroovyExtendableScriptCache has no other external dependencies other than Groovy itself,
068 * so can be used independent of Commons SCXML.
069 * </p>
070 */
071public class GroovyExtendableScriptCache implements Serializable {
072
073    private static final long serialVersionUID = 1L;
074
075    /**
076     * Serializable factory interface providing the Groovy parent ClassLoader,
077     * needed to restore the specific ClassLoader after de-serialization
078     */
079    public interface ParentClassLoaderFactory extends Serializable {
080        ClassLoader getClassLoader();
081    }
082
083    /**
084     * Serializable factory interface providing the Groovy CompilerConfiguration,
085     * needed to restore the specific CompilerConfiguration after de-serialization
086     */
087    public interface CompilerConfigurationFactory extends Serializable {
088        CompilerConfiguration getCompilerConfiguration();
089    }
090
091    public interface ScriptPreProcessor extends Serializable {
092        String preProcess(String script);
093    }
094
095    /** Default CodeSource code base for the compiled Groovy scripts */
096    public static final String DEFAULT_SCRIPT_CODE_BASE = "/groovy/scxml/script";
097
098    /** Default factory for the Groovy parent ClassLoader, returning this class its ClassLoader */
099    public static final ParentClassLoaderFactory DEFAULT_PARENT_CLASS_LOADER_FACTORY = new ParentClassLoaderFactory() {
100        public ClassLoader getClassLoader() {
101            return GroovyExtendableScriptCache.class.getClassLoader();
102        }
103    };
104
105    /** Default factory for the Groovy CompilerConfiguration, returning a new and unmodified CompilerConfiguration instance */
106    public static final CompilerConfigurationFactory DEFAULT_COMPILER_CONFIGURATION_FACTORY = new CompilerConfigurationFactory() {
107        public CompilerConfiguration getCompilerConfiguration() {
108            return new CompilerConfiguration();
109        }
110    };
111
112    protected static class ScriptCacheElement implements Serializable {
113        private static final long serialVersionUID = 1L;
114
115        protected final String baseClass;
116        protected final String scriptSource;
117        protected String scriptName;
118        protected transient Class<? extends Script> scriptClass;
119
120        public ScriptCacheElement(String baseClass, String scriptSource) {
121            this.baseClass = baseClass;
122            this.scriptSource = scriptSource;
123        }
124
125        public String getBaseClass() {
126            return baseClass;
127        }
128
129        public String getScriptSource() {
130            return scriptSource;
131        }
132
133        public String getScriptName() {
134            return scriptName;
135        }
136
137        public void setScriptName(String scriptName) {
138            this.scriptName = scriptName;
139        }
140
141        public Class<? extends Script> getScriptClass() {
142            return scriptClass;
143        }
144
145        public void setScriptClass(Class<? extends Script> scriptClass) {
146            this.scriptClass = scriptClass;
147        }
148
149        @Override
150        public boolean equals(final Object o) {
151            if (this == o) {
152                return true;
153            }
154            if (o == null || getClass() != o.getClass()) {
155                return false;
156            }
157
158            final ScriptCacheElement that = (ScriptCacheElement) o;
159
160            return !(baseClass != null ? !baseClass.equals(that.baseClass) : that.baseClass != null) &&
161                    scriptSource.equals(that.scriptSource);
162
163        }
164
165        @Override
166        public int hashCode() {
167            int result = baseClass != null ? baseClass.hashCode() : 0;
168            result = 31 * result + scriptSource.hashCode();
169            return result;
170        }
171    }
172
173    private final LinkedHashMap<ScriptCacheElement, ScriptCacheElement> scriptCache = new LinkedHashMap<ScriptCacheElement, ScriptCacheElement>();
174
175    private String scriptCodeBase = DEFAULT_SCRIPT_CODE_BASE;
176    private String scriptBaseClass;
177    private ParentClassLoaderFactory parentClassLoaderFactory = DEFAULT_PARENT_CLASS_LOADER_FACTORY;
178    private CompilerConfigurationFactory compilerConfigurationFactory = DEFAULT_COMPILER_CONFIGURATION_FACTORY;
179    private ScriptPreProcessor scriptPreProcessor;
180
181    /* non-serializable thus transient GroovyClassLoader and CompilerConfiguration */
182    private transient GroovyClassLoader groovyClassLoader;
183    private transient CompilerConfiguration compilerConfiguration;
184
185    public GroovyExtendableScriptCache() {
186    }
187
188    /**
189     * Hook into the de-serialization process, reloading the transient GroovyClassLoader, CompilerConfiguration and
190     * re-generate Script classes through {@link #ensureInitializedOrReloaded()}
191     */
192    private void readObject(ObjectInputStream in) throws IOException,ClassNotFoundException {
193        in.defaultReadObject();
194        ensureInitializedOrReloaded();
195    }
196
197    public ClassLoader getGroovyClassLoader() {
198        return groovyClassLoader;
199    }
200
201    /**
202     * @param scriptSource The script source, which will optionally be first preprocessed through {@link #preProcessScript(String)}
203     *                     using the configured {@link #getScriptPreProcessor}
204     * @return A new Script instance from a compiled (or cached) Groovy class parsed from the provided
205     * scriptSource
206     */
207    public Script getScript(String scriptSource) {
208        return getScript(null, scriptSource);
209    }
210
211    public Script getScript(String scriptBaseClass, String scriptSource) {
212        Class<? extends Script> scriptClass;
213        synchronized (scriptCache) {
214            ensureInitializedOrReloaded();
215            ScriptCacheElement cacheKey = new ScriptCacheElement(scriptBaseClass, scriptSource);
216            ScriptCacheElement cacheElement = scriptCache.get(cacheKey);
217            if (cacheElement != null) {
218                scriptClass = cacheElement.getScriptClass();
219            }
220            else {
221                String scriptName = generatedScriptName(scriptSource, scriptCache.size());
222                scriptClass = compileScript(scriptBaseClass, scriptSource, scriptName);
223                cacheKey.setScriptName(scriptName);
224                cacheKey.setScriptClass(scriptClass);
225                scriptCache.put(cacheKey, cacheKey);
226            }
227        }
228        try {
229            return scriptClass.newInstance();
230        } catch (Exception e) {
231            throw new GroovyRuntimeException("Failed to create Script instance for class: "+ scriptClass + ". Reason: " + e, e);
232        }
233    }
234
235    protected void ensureInitializedOrReloaded() {
236        if (groovyClassLoader == null) {
237            compilerConfiguration = new CompilerConfiguration(getCompilerConfigurationFactory().getCompilerConfiguration());
238            if (getScriptBaseClass() != null) {
239                compilerConfiguration.setScriptBaseClass(getScriptBaseClass());
240            }
241
242            groovyClassLoader = AccessController.doPrivileged(new PrivilegedAction<GroovyClassLoader>() {
243                public GroovyClassLoader run() {
244                    return new GroovyClassLoader(getParentClassLoaderFactory().getClassLoader(), compilerConfiguration);
245                }
246            });
247            if (!scriptCache.isEmpty()) {
248                // de-serialized: need to re-generate all previously compiled scripts (this can cause a hick-up...):
249                for (ScriptCacheElement element : scriptCache.keySet()) {
250                    element.setScriptClass(compileScript(element.getBaseClass(), element.getScriptSource(), element.getScriptName()));
251                }
252            }
253        }
254    }
255
256    @SuppressWarnings("unchecked")
257    protected Class<Script> compileScript(final String scriptBaseClass, String scriptSource, final String scriptName) {
258        final String script = preProcessScript(scriptSource);
259
260        GroovyCodeSource codeSource = AccessController.doPrivileged(new PrivilegedAction<GroovyCodeSource>() {
261            public GroovyCodeSource run() {
262                return new GroovyCodeSource(script, scriptName, getScriptCodeBase());
263            }
264        });
265
266        String currentScriptBaseClass = compilerConfiguration.getScriptBaseClass();
267        try {
268            if (scriptBaseClass != null) {
269                compilerConfiguration.setScriptBaseClass(scriptBaseClass);
270            }
271            return groovyClassLoader.parseClass(codeSource, false);
272        }
273        finally {
274            compilerConfiguration.setScriptBaseClass(currentScriptBaseClass);
275        }
276    }
277
278    protected String preProcessScript(String scriptSource) {
279        return getScriptPreProcessor() != null ? getScriptPreProcessor().preProcess(scriptSource) : scriptSource;
280    }
281
282    protected String generatedScriptName(String scriptSource, int seed) {
283        return "script"+seed+"_"+Math.abs(scriptSource.hashCode())+".groovy";
284    }
285
286    /** @return The current configured CodeSource code base used for the compilation of the Groovy scripts */
287    public String getScriptCodeBase() {
288        return scriptCodeBase;
289    }
290
291    /**
292     * @param scriptCodeBase The CodeSource code base to be used for the compilation of the Groovy scripts.<br/>
293     *                             When null, of zero length or not (at least) starting with a '/',
294     *                             the {@link #DEFAULT_SCRIPT_CODE_BASE} will be set instead.
295     */
296    @SuppressWarnings("unused")
297    public void setScriptCodeBase(String scriptCodeBase) {
298        if (scriptCodeBase != null && scriptCodeBase.length() > 0 && scriptCodeBase.charAt(0) == '/') {
299            this.scriptCodeBase = scriptCodeBase;
300        }
301        else {
302            this.scriptCodeBase = DEFAULT_SCRIPT_CODE_BASE;
303        }
304    }
305
306    public String getScriptBaseClass() {
307        return scriptBaseClass;
308    }
309
310    public void setScriptBaseClass(String scriptBaseClass) {
311        this.scriptBaseClass = scriptBaseClass;
312    }
313
314    public ParentClassLoaderFactory getParentClassLoaderFactory() {
315        return parentClassLoaderFactory;
316    }
317
318    @SuppressWarnings("unused")
319    public void setParentClassLoaderFactory(ParentClassLoaderFactory parentClassLoaderFactory) {
320        this.parentClassLoaderFactory = parentClassLoaderFactory != null ? parentClassLoaderFactory : DEFAULT_PARENT_CLASS_LOADER_FACTORY;
321    }
322
323    public CompilerConfigurationFactory getCompilerConfigurationFactory() {
324        return compilerConfigurationFactory;
325    }
326
327    @SuppressWarnings("unused")
328    public void setCompilerConfigurationFactory(CompilerConfigurationFactory compilerConfigurationFactory) {
329        this.compilerConfigurationFactory = compilerConfigurationFactory != null ? compilerConfigurationFactory : DEFAULT_COMPILER_CONFIGURATION_FACTORY;
330    }
331
332    public ScriptPreProcessor getScriptPreProcessor() {
333        return scriptPreProcessor;
334    }
335
336    public void setScriptPreProcessor(final ScriptPreProcessor scriptPreProcessor) {
337        this.scriptPreProcessor = scriptPreProcessor;
338    }
339
340    public boolean isEmpty() {
341        synchronized (scriptCache) {
342            return scriptCache.isEmpty();
343        }
344    }
345    public void clearCache() {
346        synchronized (scriptCache) {
347            scriptCache.clear();
348            if (groovyClassLoader != null) {
349                groovyClassLoader.clearCache();
350            }
351        }
352    }
353}