Java tutorial
/** * Helios, OpenSource Monitoring * Brought to you by the Helios Development Group * * Copyright 2014, Helios Development Group and individual contributors * as indicated by the @author tags. See the copyright.txt file in the * distribution for a full listing of individual contributors. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. * */ package com.heliosapm.script; import groovy.lang.Script; import java.io.File; import java.io.FileInputStream; import java.io.InputStream; import java.io.InputStreamReader; import java.lang.management.ManagementFactory; import java.net.URL; import java.net.URLClassLoader; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.atomic.AtomicBoolean; import javax.management.MBeanNotificationInfo; import javax.management.NotificationBroadcasterSupport; import javax.script.Bindings; import javax.script.Compilable; import javax.script.CompiledScript; import javax.script.ScriptContext; import javax.script.ScriptEngine; import javax.script.ScriptEngineFactory; import javax.script.ScriptEngineManager; import javax.script.ScriptException; import javax.script.SimpleBindings; import org.cliffc.high_scale_lib.NonBlockingHashMap; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import com.google.common.cache.RemovalListener; import com.google.common.cache.RemovalNotification; import com.heliosapm.jmx.cache.CacheStatistics; import com.heliosapm.jmx.config.Configuration; import com.heliosapm.jmx.notif.SharedNotificationExecutor; import com.heliosapm.jmx.util.helpers.ArrayUtils; import com.heliosapm.jmx.util.helpers.ConfigurationHelper; import com.heliosapm.jmx.util.helpers.JMXHelper; import com.heliosapm.jmx.util.helpers.URLHelper; import com.heliosapm.script.compilers.CompilerException; import com.heliosapm.script.compilers.ConfigurationCompiler; import com.heliosapm.script.compilers.DeploymentCompiler; import com.heliosapm.script.compilers.FixtureCompiler; import com.heliosapm.script.compilers.GroovyCompiler; import com.heliosapm.script.compilers.JSR223Compiler; import com.heliosapm.script.compilers.groovy.ConfigurableGroovyScriptEngineFactory; /** * <p>Title: StateService</p> * <p>Description: Singleton service for saving state and providing numerical deltas</p> * <p>Company: Helios Development Group LLC</p> * @author Whitehead (nwhitehead AT heliosdev DOT org) * <p><code>com.heliosapm.script.StateService</code></p> */ public class StateService extends NotificationBroadcasterSupport implements StateServiceMXBean, RemovalListener<Object, Object> { /** The singleton instance */ private static volatile StateService instance = null; /** The singleton instance ctor lock */ private static final Object lock = new Object(); /** The number of processors in the current JVM */ public static final int CORES = ManagementFactory.getOperatingSystemMXBean().getAvailableProcessors(); /** The descriptors of the JMX notifications emitted by this service */ private static final MBeanNotificationInfo[] notificationInfos = new MBeanNotificationInfo[] { // TODO: add infos }; /** The conf property name for a list of comma separated URLs to additional classpaths to add to the script engine factory classloader */ public static final String SCRIPT_CLASSPATH_PROP = "com.heliosapm.jmx.stateservice.classpath"; /** The conf property name for the cache spec for the simple object cache */ public static final String STATE_CACHE_PROP = "com.heliosapm.jmx.stateservice.simplecachespec"; /** The conf property name for the cache spec for the long delta cache */ public static final String STATE_LONG_CACHE_PROP = "com.heliosapm.jmx.stateservice.longcachespec"; /** The conf property name for the cache spec for the double delta cache */ public static final String STATE_DOUBLE_CACHE_PROP = "com.heliosapm.jmx.stateservice.doublecachespec"; /** The conf property name for the cache spec for the script cache */ public static final String STATE_SCRIPT_CACHE_PROP = "com.heliosapm.jmx.stateservice.scriptcachespec"; /** The conf property name for the cache spec for the deployment cache */ public static final String STATE_DEPLOYMENT_CACHE_PROP = "com.heliosapm.jmx.stateservice.deploymentcachespec"; /** The conf property name for the cache spec for the script binding cache */ public static final String STATE_BINDING_CACHE_PROP = "com.heliosapm.jmx.stateservice.bindingcachespec"; /** The default cache spec */ public static final String STATE_CACHE_DEFAULT_SPEC = "concurrencyLevel=" + CORES + "," + "initialCapacity=256," + "maximumSize=5120," + "expireAfterWrite=15m," + "expireAfterAccess=15m," + "weakValues" + ",recordStats"; /** A set of javascript helper source code file names */ public static final Set<String> JS_HELPERS = Collections.unmodifiableSet(new HashSet<String>(Arrays.asList( //"math.js", "helpers.js"))); /** The script engine manager */ private final ScriptEngineManager sem; /** The simple state cache */ private final Cache<Object, Object> simpleStateCache = CacheStatistics.getJMXStatisticsEnableCache( CacheBuilder .from(ConfigurationHelper.getSystemThenEnvProperty(STATE_CACHE_PROP, STATE_CACHE_DEFAULT_SPEC)), "state"); /** The cache for long deltas */ private final Cache<Object, long[]> longDeltaCache = CacheStatistics.getJMXStatisticsEnableCache( CacheBuilder.from( ConfigurationHelper.getSystemThenEnvProperty(STATE_LONG_CACHE_PROP, STATE_CACHE_DEFAULT_SPEC)), "longDeltas"); /** The cache for double deltas */ private final Cache<Object, double[]> doubleDeltaCache = CacheStatistics .getJMXStatisticsEnableCache(CacheBuilder.from(ConfigurationHelper .getSystemThenEnvProperty(STATE_DOUBLE_CACHE_PROP, STATE_CACHE_DEFAULT_SPEC)), "doubleDeltas"); /** The compiled script cache keyed by the script extension */ private final Cache<String, Cache<String, CompiledScript>> scriptCache = CacheStatistics .getJMXStatisticsEnableCache( CacheBuilder.newBuilder().concurrencyLevel(CORES).initialCapacity(16).recordStats(), "script"); /** The scoped script bindings cache */ private final Cache<Object, Bindings> bindingsCache = CacheStatistics.getJMXStatisticsEnableCache(CacheBuilder .from(ConfigurationHelper.getSystemThenEnvProperty(STATE_BINDING_CACHE_PROP, STATE_CACHE_DEFAULT_SPEC)), "bindings"); //=========================================================================================== // Deployment Management //=========================================================================================== /** The compiled deployment cache */ private final Cache<String, DeployedScript<?>> deploymentCache = CacheBuilder.from( ConfigurationHelper.getSystemThenEnvProperty(STATE_DEPLOYMENT_CACHE_PROP, STATE_CACHE_DEFAULT_SPEC)) .build(); /** The deployment compilers keyed by script extension */ private final Map<String, DeploymentCompiler<?>> deploymentCompilers = new NonBlockingHashMap<String, DeploymentCompiler<?>>( 16); /** The catch-all deployment compiler */ private final DeploymentCompiler<CompiledScript> catchAllCompiler; /** The groovy deployment compiler */ private final DeploymentCompiler<Script> groovyCompiler; /** The groovy jsr223 ScriptEngine */ private final ScriptEngine groovyConfigurableCompiler; /** The configuration deployment compiler */ private final DeploymentCompiler<Configuration> configurationCompiler; //=========================================================================================== /** Instance logger */ private final Logger log = LoggerFactory.getLogger(getClass()); // /** The script engine */ // private final ScriptEngine engine; // /** The script compiler */ // private final Compilable compiler; /** The script engines keyed by script extension */ private final Map<String, ScriptEngineFactory> engines = new NonBlockingHashMap<String, ScriptEngineFactory>( 16); /** The global ({@link ScriptEngineFactory}) level bindings (shared amongst all scripts) */ private final Bindings engineBindings; /** Singleton ctor reentrancy check */ private static final AtomicBoolean initing = new AtomicBoolean(false); /** Reserved extensions for built in deployment types */ public static final Set<String> reservedExtensions = Collections .unmodifiableSet(new HashSet<String>(Arrays.asList("config", // Configuration deployments "pool", // Pooled resource "factory", // object factory "datasource" // JDBC data source ))); /** * Acquires the StateService singleton instance * @return the StateService singleton instance */ public static StateService getInstance() { if (instance == null) { synchronized (lock) { if (instance == null) { if (!initing.compareAndSet(false, true)) { throw new RuntimeException( "Reentrant call to StateService.getInstance(). Programmer Error."); } instance = new StateService(); } } } return instance; } /** * Determines if a script engine is registered that can handle a script of the passed extension * @param extension The extension to test * @return true if supported, false otherwise */ public boolean isExtensionSupported(final String extension) { if (extension == null || extension.trim().isEmpty()) return false; //throw new IllegalArgumentException("The passed extension was null or empty"); return engines.containsKey(extension.trim().toLowerCase()); } /** * Installs a deployment compiler * @param deploymentCompiler The deployment compiler to install */ protected void installDeploymentCompiler(final DeploymentCompiler<?> deploymentCompiler) { if (deploymentCompiler == null) throw new IllegalArgumentException("DeploymentCompiler was null"); final String[] extensions = deploymentCompiler.getSupportedExtensions(); for (String extension : extensions) { deploymentCompilers.put(extension, deploymentCompiler); DeploymentType.addScriptExtension(extension); } log.info("Installed DeploymentCompiler [{}] for extensions {}", deploymentCompiler.getClass().getSimpleName(), Arrays.toString(extensions)); } /** * Returns the cached compiled script for the passed source code, compiling it if it does not exist * @param extension The script extension * @param code The source code to get the compiled script for * @return the compiled script */ public CompiledScript getCompiledScript(final String extension, final String code) { if (code == null || code.trim().isEmpty()) throw new IllegalArgumentException("The passed code was null or empty"); if (extension == null || extension.trim().isEmpty()) throw new IllegalArgumentException("The passed extension was null or empty"); final String key = extension.trim().toLowerCase(); final Compilable compiler = getCompilerForExtension(key); try { final Cache<String, CompiledScript> extensionCache = scriptCache.get(key, new Callable<Cache<String, CompiledScript>>() { @Override public Cache<String, CompiledScript> call() throws Exception { return CacheBuilder.from(ConfigurationHelper .getSystemThenEnvProperty(STATE_SCRIPT_CACHE_PROP, STATE_CACHE_DEFAULT_SPEC)) .build(); } }); return extensionCache.get(code, new Callable<CompiledScript>() { @Override public CompiledScript call() throws Exception { return compiler.compile(code); } }); } catch (Exception ex) { if (ex instanceof ScriptException) { throw new RuntimeException("Exception compiling script for [" + code + "]", ex); } throw new RuntimeException("Unexpected exception getting compiled script for [" + code + "]", ex); } } /** * Saves a simple keyed state * @param key The key for the saved state * @param value The value for the saved state * @return The value previously bound to the passed key, or null if there was none */ public <T> T put(final Object key, final T value) { if (key == null) throw new IllegalArgumentException("The passed key was null"); if (value == null) throw new IllegalArgumentException("The passed value was null"); return (T) simpleStateCache.asMap().put(key, value); } /** * Computes and returns an elapsed time * @param key The key * @param time The time that defines the end of the elapsed * @return the elapsed time or null if no starting time was in state */ public Long elapsedTime(final Object key, final long time) { return delta(key, time); } /** * Computes and returns an elapsed time using the current time as the end time * @param key The key * @return the elapsed time or null if no starting time was in state */ public Long elapsedTime(final Object key) { return delta(key, System.currentTimeMillis()); } /** * Returns the cached bindings for the passed key, creating new bindings if not found * @param key The key * @return the cached bindings */ public Bindings getBindings(final Object key) { if (key == null) throw new IllegalArgumentException("The passed key was null"); try { return bindingsCache.get(key, new Callable<Bindings>() { @Override public Bindings call() throws Exception { Bindings b = new SimpleBindings(); b.put("bindings", b); b.put("bindingsKey", key); b.put("stateService", getInstance()); return b; } }); } catch (Exception ex) { throw new RuntimeException("Unexpected exception getting bindings for key [" + key + "]", ex); } } /** * Caches the passed value and if it replaces an existing value, the delta of the two values is returned. * Otherwise null is returned. * @param key The key for this delta * @param value The value to store and delta * @return the delta value or null if no value was already in state */ public Long delta(final Object key, final long value) { if (key == null) throw new IllegalArgumentException("The passed key was null"); final long[] state = longDeltaCache.asMap().put(key, new long[] { value }); if (state == null) return null; return value - state[0]; } /** * Implements a resetting delta where the state is reset to the specified value if the incoming value * is less than the value in state. When this occurs, a null is returned. Otherwise behaves like {@link #delta(Object, long)} * @param key The key * @param value The incoming value to delta * @param resetValue The value to reset the state to when the delta is reset * @return the delta value or null if there was no value in state, or the delta was reset */ public Long rdelta(final Object key, final long value, final long resetValue) { if (key == null) throw new IllegalArgumentException("The passed key was null"); final long[] state = longDeltaCache.asMap().put(key, new long[] { value }); if (state == null) return null; if (value < state[0]) { longDeltaCache.asMap().put(key, new long[] { resetValue }); return null; } return value - state[0]; } /** * Caches the passed value and if it replaces an existing value, the delta of the two values is returned. * Otherwise null is returned. * @param key The key for this delta * @param value The value to store and delta * @return the delta value or null if no value was already in state */ public Double delta(final Object key, final double value) { if (key == null) throw new IllegalArgumentException("The passed key was null"); final double[] state = doubleDeltaCache.asMap().put(key, new double[] { value }); if (state == null) return null; return value - state[0]; } /** * Implements a resetting delta where the state is reset to the specified value if the incoming value * is less than the value in state. When this occurs, a null is returned. Otherwise behaves like {@link #delta(Object, long)} * @param key The key * @param value The incoming value to delta * @param resetValue The value to reset the state to when the delta is reset * @return the delta value or null if there was no value in state, or the delta was reset */ public Double rdelta(final Object key, final double value, final double resetValue) { if (key == null) throw new IllegalArgumentException("The passed key was null"); final double[] state = doubleDeltaCache.asMap().put(key, new double[] { value }); if (state == null) return null; if (value < state[0]) { doubleDeltaCache.asMap().put(key, new double[] { resetValue }); return null; } return value - state[0]; } /** * Returns the state object saved under the passed key * @param key The key * @param defaultValue The default value to return if the key is not bound * @param type The type of the state object stored * @return the state object or the defined default if it was not found */ public <T> T get(final Object key, final T defaultValue, final Class<T> type) { T t = (T) simpleStateCache.asMap().get(key); return t != null ? t : defaultValue; } /** * Returns the state object saved under the passed key * @param key The key * @param type The expected type of the bound value * @return the cached value or null if not bound */ public <T> T get(final Object key, final Class<T> type) { return get(key, null, type); } /** * Returns the state object saved under the passed key * @param key The key * @param defaultValue The default value to return if the key is not bound * @return the state object or the defined default if it was not found */ public Object get(final Object key, final Object defaultValue) { return get(key, defaultValue, Object.class); } /** * Returns the state object saved under the passed key * @param key The key * @return the state object or the null if it was not found */ public Object get(final Object key) { return get(key, null, Object.class); } public static void main(String[] args) { getInstance(); } /** * Returns the ScriptEngine for the passed script extension * @param extension The script extension * @return the associated ScriptEngine */ public ScriptEngine getEngineForExtension(final String extension) { if (extension == null || extension.trim().isEmpty()) throw new IllegalArgumentException("The passed extension was null"); final String key = extension.trim().toLowerCase(); ScriptEngineFactory sef = engines.get(key); if (sef == null) { synchronized (engines) { sef = engines.get(key); if (sef == null) { try { ScriptEngine se = sem.getEngineByExtension(key); sef = se.getFactory(); se.setBindings(engineBindings, ScriptContext.GLOBAL_SCOPE); } catch (Exception ex) { throw new RuntimeException("No script engine found for extension [" + key + "]"); } installScriptEngineFactory(sef); } } } return sef.getScriptEngine(); } /** * Retrieves the executable deployed script for the passed source file, creating it if it does not exist * @param sourceFile The source file to the the deployed script for * @return the deployed script instance */ public <T> DeployedScript<T> getDeployedScript(final String sourceFile) { if (sourceFile == null || sourceFile.trim().isEmpty()) throw new IllegalArgumentException("The passed source file was null or empty"); final File f = new File(sourceFile.trim()).getAbsoluteFile(); if (!f.exists()) throw new IllegalArgumentException("The passed file [" + f + "] does not exist"); if (!f.isFile()) throw new IllegalArgumentException("The passed file [" + f + "] is *not* a regular file"); final AtomicBoolean newDs = new AtomicBoolean(false); // TODO: Check for linked files ! // TODO: Events // TODO: Schedule // TODO: Pre-Check Extension Support // TODO: File Deletion --> Undeploy try { DeployedScript<T> deployedScript = (DeployedScript<T>) deploymentCache.get(f.getAbsolutePath(), new Callable<DeployedScript<T>>() { @Override public DeployedScript<T> call() throws Exception { try { DeploymentCompiler<T> compiler = (DeploymentCompiler<T>) deploymentCompilers .get(URLHelper.getFileExtension(f)); if (compiler == null) { compiler = (DeploymentCompiler<T>) catchAllCompiler; } DeployedScript<T> ds = compiler.deploy(sourceFile); if (!JMXHelper.isRegistered(ds.getObjectName())) { JMXHelper.registerMBean(ds.getObjectName(), ds); } newDs.set(true); return ds; } catch (Exception ex) { log.error("Failed to deploy [{}]", sourceFile, ex); throw ex; } } }); if (!newDs.get()) { final long ad32 = URLHelper.adler32(f); final long lastMod = URLHelper.getLastModified(f); if (ad32 != deployedScript.getChecksum() || lastMod < deployedScript.getLastModified()) { synchronized (deployedScript) { if (ad32 != deployedScript.getChecksum() || lastMod < deployedScript.getLastModified()) { try { DeploymentCompiler<T> compiler = (DeploymentCompiler<T>) deploymentCompilers .get(URLHelper.getFileExtension(f)); if (compiler == null) { compiler = (DeploymentCompiler<T>) catchAllCompiler; } T exe = compiler.compile(URLHelper.toURL(sourceFile)); deployedScript.setExecutable(exe, ad32, lastMod); } catch (CompilerException cex) { deployedScript.setFailedExecutable(cex.getDiagnostic(), ad32, lastMod); } catch (Exception ex) { log.error("Failed to deploy [{}]", sourceFile, ex); throw ex; } } } } } return deployedScript; } catch (Exception ex) { throw new RuntimeException("Failed to get deployment script for file [" + sourceFile + "]", ex); } } /** * Returns the DeploymentCompiler for the passed script extension * @param extension The script extension * @return the associated DeploymentCompiler */ public Compilable getCompilerForExtension(final String extension) { if (extension == null || extension.trim().isEmpty()) throw new IllegalArgumentException("The passed extension was null"); final String key = extension.trim().toLowerCase(); ScriptEngineFactory sef = engines.get(key); ScriptEngine se = sef.getScriptEngine(); if (!(se instanceof Compilable)) { throw new RuntimeException("Script engine does not support Compilable\n" + renderEngineFactory(sef)); } return (Compilable) se; } /** * Returns an array of the installed engine extensions * @return an array of the installed engine extensions */ public String[] getInstalledExtensions() { return engines.keySet().toArray(new String[0]); } /** * Determines if the passed extension is supported by an installed engine * @param extension The extension to test * @return true if the passed extension is supported by an installed engine, false otherwise */ public boolean isExtensionInstalled(final String extension) { if (extension == null || extension.trim().isEmpty()) return false; return engines.containsKey(extension); } /** * Finds a JS script engine that works with <a href="http://mathjs.org/">math.js</a> * due to a <a href="https://github.com/mozilla/rhino/issues/127">JDK Rhino issue</a> * @return A JS script engine that works or null if one could not be found */ private ScriptEngineFactory findEngine() { final String javaHome = URLHelper.toURL(new File(System.getProperty("java.home"))).toString().toLowerCase(); ScriptEngine se = null; Set<ScriptEngineFactory> nativeFirstScriptEngines = new LinkedHashSet<ScriptEngineFactory>(); List<ScriptEngineFactory> tmp = new ArrayList<ScriptEngineFactory>(sem.getEngineFactories()); for (Iterator<ScriptEngineFactory> iter = tmp.iterator(); iter.hasNext();) { final ScriptEngineFactory sef = iter.next(); if (!sef.getExtensions().contains("js")) { iter.remove(); continue; } try { String codeSource = sef.getScriptEngine().getClass().getProtectionDomain().getCodeSource() .getLocation().toString(); if (codeSource.toLowerCase().startsWith(javaHome)) throw new Exception(); } catch (Exception ex) { // some classes in system will throw an exception nativeFirstScriptEngines.add(sef); iter.remove(); } } nativeFirstScriptEngines.addAll(tmp); for (ScriptEngineFactory sef : nativeFirstScriptEngines) { try { if (!sef.getExtensions().contains("js")) continue; se = sef.getScriptEngine(); se.eval("var obj = {};"); se.eval("obj.boolean = 2;"); // error if engine has bug return sef; } catch (Exception ex) { se = null; log.warn("Discarding engine [{}] v. [{}]", sef.getEngineName(), sef.getEngineVersion()); } } throw new RuntimeException( "No compatible script engine found. Try Nashorn, Rhino or see https://github.com/mozilla/rhino/issues/127"); } private void installScriptEngineFactory(final ScriptEngineFactory sef) { final Set<String> addedExtensions = new HashSet<String>(); for (String ext : sef.getExtensions()) { String extKey = ext.trim().toLowerCase(); if (reservedExtensions.contains(extKey)) { // TODO: allow alias definitions in the unlikely case this occurs log.warn("The ScriptEngineFactory extension [{}] for [{}] is a reserved extension", extKey, sef.getClass().getName()); continue; } if (!engines.containsKey(extKey)) { synchronized (engines) { if (!engines.containsKey(extKey)) { engines.put(extKey, sef); addedExtensions.add(extKey); // ConfigurationCompiler.addSubExtension(extKey); } } } } log.info("Installed ScriptEngine\n{}", renderEngineFactory(sef, addedExtensions)); } /** * Creates a new StateService */ private StateService() { super(SharedNotificationExecutor.getInstance(), notificationInfos); ConfigurableGroovyScriptEngineFactory cgsef = new ConfigurableGroovyScriptEngineFactory(); groovyConfigurableCompiler = new ConfigurableGroovyScriptEngineFactory().getScriptEngine(); installScriptEngineFactory(cgsef); sem = new ScriptEngineManager(getScriptClasspath()); engineBindings = new SimpleBindings(); engineBindings.put("stateService", this); // need to get a specific JS engine installScriptEngineFactory(findEngine()); for (ScriptEngineFactory foundSef : sem.getEngineFactories()) { if (foundSef.getExtensions().contains("js")) continue; try { for (String ext : foundSef.getExtensions()) { if (!engines.containsKey(ext.trim().toLowerCase())) { installScriptEngineFactory(foundSef); break; } } log.info("Located Additional ScriptEngine Impl:{}", foundSef.getScriptEngine()); } catch (Throwable ex) { log.warn("Failed to install SEF [{}]. Skipping.", foundSef.getClass().getName()); } } loadJavaScriptHelpers(); groovyCompiler = new GroovyCompiler(); installDeploymentCompiler(groovyCompiler); catchAllCompiler = new JSR223Compiler(this); configurationCompiler = new ConfigurationCompiler(); final FixtureCompiler<Object> fixtureCompiler = new FixtureCompiler<Object>(this); installDeploymentCompiler(fixtureCompiler); deploymentCompilers.put("config", configurationCompiler); deploymentCompilers.put("fixture", fixtureCompiler); JMXHelper.registerMBean(this, OBJECT_NAME); } /** * Checks for sysprop/env defined extra classpath for the script engines and installs it if found * @return the class loader to use for the script engine manager */ private ClassLoader getScriptClasspath() { String[] paths = ArrayUtils .trim(ConfigurationHelper.getSystemThenEnvProperty(SCRIPT_CLASSPATH_PROP, "").split(",")); if (paths.length == 0) return Thread.currentThread().getContextClassLoader(); Set<URL> urls = new HashSet<URL>(); for (String path : paths) { try { URL url = URLHelper.toURL(path); if (URLHelper.resolves(url)) { urls.add(url); } } catch (Exception x) { /* No Op */ } } if (urls.isEmpty()) return Thread.currentThread().getContextClassLoader(); log.info("Script Extra Classpath: {}", urls.toString()); return new URLClassLoader(urls.toArray(new URL[urls.size()]), Thread.currentThread().getContextClassLoader()); } private boolean areWeJarred() { final String myClassPath = getClass().getProtectionDomain().getCodeSource().getLocation().toString(); return myClassPath.toLowerCase().endsWith(".jar"); } private void loadJavaScriptHelpers() { final Thread jsLoader = new Thread("JSLoaderThread") { public void run() { try { final long start = System.currentTimeMillis(); final boolean inJar = areWeJarred(); final ScriptEngine se = getEngineForExtension("js"); for (String fileName : JS_HELPERS) { log.info("Loading JS Helper [{}]", fileName); try { loadJavaScriptFrom(se, inJar, "/javascript/" + fileName, "./src/main/resources/javascript/" + fileName); } catch (Exception ex) { log.error("Failed to load JS Helper [{}]", fileName, ex); } } final String expected = "{\"foo\":123}"; boolean jsonSupport = testScript(se, "var a = {\"foo\": 123}; JSON.stringify(a);", expected); if (!jsonSupport) { log.info("JSON.stringify not implemented. Loading backup"); loadJavaScriptFrom(se, inJar, "/javascript/json/json2.js", "./src/main/resources/javascript/json/json2.js"); jsonSupport = testScript(se, "var a = {'\"foo\": 123}; JSON.stringify(a);", expected); } log.info("JSON.stringify supported after backup: {}", jsonSupport); final long elapsed = System.currentTimeMillis() - start; log.info( "\n\t=====================================\n\tJSHelpers loaded in [{}] ms.\n\t=====================================\n", elapsed); } catch (Exception ex) { log.error("JavaScript Loader Failed !", ex); } } }; jsLoader.setDaemon(true); jsLoader.start(); } private void loadJavaScriptFrom(final ScriptEngine se, final boolean inJar, final String jarPath, final String devPath) { InputStream is = null; InputStreamReader isReader = null; String path = inJar ? jarPath : devPath; try { if (inJar) { is = getClass().getClassLoader().getResourceAsStream(path); } else { is = new FileInputStream(path); } if (is == null) { throw new Exception("Could not find JS Helper File [" + path + "]"); } isReader = new InputStreamReader(is); try { ((Compilable) se).compile(isReader); log.info("Compiled [{}]", path); } catch (Exception ex) { log.info("Compilation of [{}] Failed. Using Eval", path); se.eval(isReader); } log.info("Loaded JS Helper [{}]", path); } catch (Exception ex) { throw new RuntimeException(ex); } finally { if (isReader != null) try { isReader.close(); } catch (Exception x) { /* No Op */} if (is != null) try { is.close(); } catch (Exception x) { /* No Op */} } } public static boolean testScript(final ScriptEngine se, final String script, final Object expected) { try { Object a = se.eval(script); if (expected != null) { return expected.equals(a); } return true; } catch (Exception ex) { return false; } } /** * Renders an informative string about the passed script engine * @param sef The ScriptEngineFactory to inform on * @return the ScriptEngine description */ public static String renderEngineFactory(final ScriptEngineFactory sef) { return renderEngineFactory(sef, null); } /** * Renders an informative string about the passed script engine * @param sef The ScriptEngineFactory to inform on * @param actualExtensions The actual installed extensions * @return the ScriptEngine description */ public static String renderEngineFactory(final ScriptEngineFactory sef, final Set<String> actualExtensions) { if (sef == null) throw new IllegalArgumentException("The passed script engine factory was null"); StringBuilder b = new StringBuilder("ScriptEngine ["); b.append("\n\tEngine:").append(sef.getEngineName()).append(" v.").append(sef.getEngineVersion()); b.append("\n\tLanguage:").append(sef.getLanguageName()).append(" v.").append(sef.getLanguageVersion()); b.append("\n\tExtensions:") .append(actualExtensions == null ? sef.getExtensions().toString() : actualExtensions.toString()); b.append("\n\tMIME Types:").append(sef.getMimeTypes().toString()); b.append("\n\tShort Names:").append(sef.getNames().toString()); return b.toString(); } /** * {@inheritDoc} * @see com.google.common.cache.RemovalListener#onRemoval(com.google.common.cache.RemovalNotification) */ @Override public void onRemoval(final RemovalNotification<Object, Object> notification) { } }