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}