View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *     http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.scxml2.env.groovy;
18  
19  import java.io.IOException;
20  import java.io.ObjectInputStream;
21  import java.io.Serializable;
22  import java.security.AccessController;
23  import java.security.PrivilegedAction;
24  import java.util.LinkedHashMap;
25  
26  import org.codehaus.groovy.control.CompilerConfiguration;
27  
28  import groovy.lang.GroovyClassLoader;
29  import groovy.lang.GroovyCodeSource;
30  import groovy.lang.GroovyRuntimeException;
31  import groovy.lang.Script;
32  
33  /**
34   * GroovyExtendableScriptCache is a general purpose and <em>{@link Serializable}</em> Groovy Script cache.
35   * <p>
36   * It provides automatic compilation of scripts and caches the resulting class(es) internally, and after de-serialization
37   * re-compiles the cached scripts automatically.
38   * </p>
39   * <p>
40   * It also provides easy support for (and scoped) script compilation with a specific {@link Script} base class.
41   * </p>
42   * <p>
43   * Internally it uses a non-serializable and thus transient {@link GroovyClassLoader}, {@link CompilerConfiguration} and
44   * the parent classloader to use.<br/>
45   * To be able to be serializable, the {@link GroovyClassLoader} is automatically (re)created if not defined yet, and for
46   * the  {@link CompilerConfiguration} and parent classloader it uses serializable instances of
47   * {@link CompilerConfigurationFactory} and {@link ParentClassLoaderFactory} interfaces which either can be configured
48   * or have defaults otherwise.
49   * </p>
50   * <p>
51   * The underlying {@link GroovyClassLoader} can be accessed through {@link #getGroovyClassLoader()}, which might be needed
52   * to de-serialize previously defined/created classes and objects through this class, from within a containing object
53   * readObject(ObjectInputStream in) method.<br/>
54   * For more information how this works and should be done, see:
55   * <a href="http://jira.codehaus.org/browse/GROOVY-1627">Groovy-1627: Deserialization fails to work</a>
56   * </p>
57   * <p>
58   * One other optional feature is script pre-processing which can be configured through an instance of the
59   * {@link ScriptPreProcessor} interface (also {@link Serializable} of course).<br/>
60   * When configured, the script source will be passed through the {@link ScriptPreProcessor#preProcess(String)} method
61   * before being compiled.
62   * </p>
63   * <p>
64   * The cache itself as well as the underlying GroovyClassLoader caches can be cleared through {@link #clearCache()}.
65   * </p>
66   * <p>
67   * The GroovyExtendableScriptCache has no other external dependencies other than Groovy itself,
68   * so can be used independent of Commons SCXML.
69   * </p>
70   */
71  public class GroovyExtendableScriptCache implements Serializable {
72  
73      private static final long serialVersionUID = 1L;
74  
75      /**
76       * Serializable factory interface providing the Groovy parent ClassLoader,
77       * needed to restore the specific ClassLoader after de-serialization
78       */
79      public interface ParentClassLoaderFactory extends Serializable {
80          ClassLoader getClassLoader();
81      }
82  
83      /**
84       * Serializable factory interface providing the Groovy CompilerConfiguration,
85       * needed to restore the specific CompilerConfiguration after de-serialization
86       */
87      public interface CompilerConfigurationFactory extends Serializable {
88          CompilerConfiguration getCompilerConfiguration();
89      }
90  
91      public interface ScriptPreProcessor extends Serializable {
92          String preProcess(String script);
93      }
94  
95      /** Default CodeSource code base for the compiled Groovy scripts */
96      public static final String DEFAULT_SCRIPT_CODE_BASE = "/groovy/scxml/script";
97  
98      /** Default factory for the Groovy parent ClassLoader, returning this class its ClassLoader */
99      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 }