Java tutorial
/* * Copyright 2015 PRODYNA AG * * Licensed under the Eclipse Public License (EPL), Version 1.0 (the "License"); you may not use * this file except in compliance with the License. You may obtain a copy of the License at * * https://www.eclipse.org/legal/epl-v10.html * * 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.nabucco.alfresco.enhScriptEnv.repo.script; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.URL; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; import org.alfresco.error.AlfrescoRuntimeException; import org.alfresco.processor.ProcessorExtension; import org.alfresco.repo.jscript.ClasspathScriptLocation; import org.alfresco.repo.jscript.RhinoScriptProcessor; import org.alfresco.repo.jscript.Scopeable; import org.alfresco.repo.processor.BaseProcessor; import org.alfresco.scripts.ScriptException; import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.cmr.repository.ScriptLocation; import org.alfresco.service.cmr.repository.ScriptProcessor; import org.alfresco.service.namespace.QName; import org.alfresco.util.MD5; import org.alfresco.util.ParameterCheck; import org.alfresco.util.PropertyCheck; import org.mozilla.javascript.Context; import org.mozilla.javascript.ImporterTopLevel; import org.mozilla.javascript.NativeJavaObject; import org.mozilla.javascript.Script; import org.mozilla.javascript.Scriptable; import org.mozilla.javascript.ScriptableObject; import org.mozilla.javascript.WrappedException; import org.nabucco.alfresco.enhScriptEnv.common.script.EnhancedScriptProcessor; import org.nabucco.alfresco.enhScriptEnv.common.script.ReferenceScript; import org.nabucco.alfresco.enhScriptEnv.common.script.ReferenceScript.CommonReferencePath; import org.nabucco.alfresco.enhScriptEnv.common.script.ReferenceScript.DynamicScript; import org.nabucco.alfresco.enhScriptEnv.common.script.ReferenceScript.ReferencePathType; import org.nabucco.alfresco.enhScriptEnv.common.script.ScopeContributor; import org.nabucco.alfresco.enhScriptEnv.common.script.converter.ValueConverter; import org.nabucco.alfresco.enhScriptEnv.common.script.converter.rhino.DelegatingWrapFactory; import org.nabucco.alfresco.enhScriptEnv.common.webscripts.processor.SurfReferencePath; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.InitializingBean; import org.springframework.context.ApplicationListener; import org.springframework.context.event.ContextRefreshedEvent; import org.springframework.extensions.surf.util.I18NUtil; import org.springframework.util.FileCopyUtils; /** * @author Axel Faust, <a href="http://www.prodyna.com">PRODYNA AG</a> */ public class EnhancedRhinoScriptProcessor extends BaseProcessor implements EnhancedScriptProcessor<ScriptLocation>, ScriptProcessor, InitializingBean, ApplicationListener<ContextRefreshedEvent> { private static final String NODE_REF_RESOURCE_IMPORT_PATTERN = "<import(\\s*\\n*\\s+)+resource(\\s*\\n*\\s*+)*=(\\s*\\n*\\s+)*\"(([^:]+)://([^/]+)/([^\"]+))\"(\\s*\\n*\\s+)*(/)?>"; private static final String NODE_REF_RESOURCE_IMPORT_REPLACEMENT = "importScript(\"node\", \"$4\", true);"; private static final String LEGACY_NAME_PATH_RESOURCE_IMPORT_PATTERN = "<import(\\s*\\n*\\s+)+resource(\\s*\\n*\\s+)*=(\\s*\\n*\\s+)*\"(/[^\"]+)\"(\\s*\\n*\\s+)*(/)?>"; private static final String LEGACY_NAME_PATH_RESOURCE_IMPORT_REPLACEMENT = "importScript(\"legacyNamePath\", \"$4\", true);"; private static final String CLASSPATH_RESOURCE_IMPORT_PATTERN = "<import(\\s*\\n*\\s+)+resource(\\s*\\n*\\s+)*=(\\s*\\n*\\s+)*\"classpath:(/)?([^\"]+)\"(\\s*\\n*\\s+)*(/)?>"; private static final String CLASSPATH_RESOURCE_IMPORT_REPLACEMENT = "importScript(\"classpath\", \"/$5\", true);"; private static final Logger LOGGER = LoggerFactory.getLogger(EnhancedRhinoScriptProcessor.class); private static final Logger LEGACY_CALL_LOGGER = LoggerFactory .getLogger(RhinoScriptProcessor.class.getName() + ".calls"); private static final List<ReferencePathType> REAL_PATH_SUCCESSION = Collections .<ReferencePathType>unmodifiableList(Arrays.<ReferencePathType>asList(CommonReferencePath.FILE, RepositoryReferencePath.FILE_FOLDER_PATH, SurfReferencePath.STORE)); private static final int DEFAULT_MAX_SCRIPT_CACHE_SIZE = 200; // used WeakHashMap here before to avoid accidental leaks but measures for proper cleanup have proven themselves // during tests protected final Map<Context, List<ReferenceScript>> activeScriptLocationChain = new ConcurrentHashMap<Context, List<ReferenceScript>>(); protected final Map<Context, List<List<ReferenceScript>>> recursionScriptLocationChains = new ConcurrentHashMap<Context, List<List<ReferenceScript>>>(); protected boolean shareScopes = true; protected Scriptable restrictedShareableScope; protected Scriptable unrestrictedShareableScope; protected boolean compileScripts = true; protected volatile boolean debuggerActive = false; protected boolean failoverToLessOptimization = true; protected int optimizationLevel = -1; protected ValueConverter valueConverter; protected final Map<String, Script> scriptCache = new LinkedHashMap<String, Script>(256); protected final ReadWriteLock scriptCacheLock = new ReentrantReadWriteLock(true); protected final AtomicLong dynamicScriptCounter = new AtomicLong(); protected final Map<String, Script> dynamicScriptCache = new LinkedHashMap<String, Script>(); protected final ReadWriteLock dynamicScriptCacheLock = new ReentrantReadWriteLock(true); protected int maxScriptCacheSize = DEFAULT_MAX_SCRIPT_CACHE_SIZE; protected final Collection<ScopeContributor> registeredContributors = new HashSet<ScopeContributor>(); /** * * {@inheritDoc} */ @Override public void afterPropertiesSet() { PropertyCheck.mandatory(this, "valueConverter", this.valueConverter); super.register(); } /** * * {@inheritDoc} */ @Override public void onApplicationEvent(final ContextRefreshedEvent event) { final ValueConverter previousConverter = ValueConverter.GLOBAL_CONVERTER.get(); ValueConverter.GLOBAL_CONVERTER.set(this.valueConverter); try { Context cx = Context.enter(); try { cx.setWrapFactory(new DelegatingWrapFactory()); this.restrictedShareableScope = this.setupScope(cx, false, true); } finally { Context.exit(); } cx = Context.enter(); try { cx.setWrapFactory(new DelegatingWrapFactory()); this.unrestrictedShareableScope = this.setupScope(cx, true, true); } finally { Context.exit(); } } finally { ValueConverter.GLOBAL_CONVERTER.set(previousConverter); } } /** * {@inheritDoc} */ @Override public Object execute(final ScriptLocation location, final Map<String, Object> model) { ParameterCheck.mandatory("location", location); final ReferenceScript actualScript = new ScriptLocationAdapter(location); final Script script = this.getCompiledScript(actualScript); final String debugScriptName; { final String path = location.getPath(); final int i = path.lastIndexOf('/'); debugScriptName = i != -1 ? path.substring(i + 1) : path; } final Context cx = Context.enter(); try { this.updateLocationChainsBeforeExceution(cx); this.activeScriptLocationChain.get(cx).add(actualScript); try { return this.executeScriptImpl(script, model, location.isSecure(), debugScriptName); } finally { this.updateLocationChainsAfterReturning(cx); } } finally { Context.exit(); } } /** * {@inheritDoc} */ @Override public Object execute(final NodeRef nodeRef, final QName contentProp, final Map<String, Object> model) { final NodeScriptLocation scriptLocation = new NodeScriptLocation(this.services, nodeRef, contentProp); return this.execute(scriptLocation, model); } /** * {@inheritDoc} */ @Override public Object execute(final String location, final Map<String, Object> model) { final ClasspathScriptLocation scriptLocation = new ClasspathScriptLocation(location); return this.execute(scriptLocation, model); } /** * {@inheritDoc} */ @Override public Object executeString(final String source, final Map<String, Object> model) { ParameterCheck.mandatoryString("source", source); final ReferenceScript referenceScript = this.toReferenceScript(source); final String debugScriptName = referenceScript.getFullName(); final Script script = this.getCompiledScript(referenceScript); final Context cx = Context.enter(); try { this.updateLocationChainsBeforeExceution(cx); this.activeScriptLocationChain.get(cx).add(new ReferenceScript.DynamicScript(debugScriptName, source)); try { return this.executeScriptImpl(script, model, false, debugScriptName); } finally { this.updateLocationChainsAfterReturning(cx); } } finally { Context.exit(); } } /** * * {@inheritDoc} */ @Override public Object executeInScope(final String source, final Object scope) { ParameterCheck.mandatoryString("source", source); final ReferenceScript referenceScript = this.toReferenceScript(source); final String debugScriptName = referenceScript.getFullName(); final Script script = this.getCompiledScript(referenceScript); LOGGER.info("{} Start", debugScriptName); LEGACY_CALL_LOGGER.debug("{} Start", debugScriptName); final long startTime = System.currentTimeMillis(); final ValueConverter previousConverter = ValueConverter.GLOBAL_CONVERTER.get(); ValueConverter.GLOBAL_CONVERTER.set(this.valueConverter); final Context cx = Context.enter(); try { final DelegatingWrapFactory wrapFactory = new DelegatingWrapFactory(); cx.setWrapFactory(wrapFactory); List<ReferenceScript> currentChain = this.activeScriptLocationChain.get(cx); boolean newChain = false; if (currentChain == null) { this.updateLocationChainsBeforeExceution(cx); currentChain = this.activeScriptLocationChain.get(cx); newChain = true; } // else: assume the original script chain is continued currentChain.add(new ReferenceScript.DynamicScript(debugScriptName, source)); try { final Scriptable realScope; if (scope == null) { if (this.shareScopes) { final Scriptable sharedScope = this.restrictedShareableScope; realScope = cx.newObject(sharedScope); realScope.setPrototype(sharedScope); realScope.setParentScope(null); } else { realScope = this.setupScope(cx, false, false); } } else if (!(scope instanceof Scriptable)) { realScope = new NativeJavaObject(null, scope, scope.getClass()); if (this.shareScopes) { final Scriptable sharedScope = this.restrictedShareableScope; realScope.setPrototype(sharedScope); } else { final Scriptable baseScope = this.setupScope(cx, false, false); realScope.setPrototype(baseScope); } } else { realScope = (Scriptable) scope; } wrapFactory.setScope(realScope); final Object result = this.executeScriptInScopeImpl(script, realScope); return result; } finally { currentChain.remove(currentChain.size() - 1); if (newChain) { this.updateLocationChainsAfterReturning(cx); } } } catch (final Exception ex) { // TODO: error handling / bubbling to caller? how to handle Rhino exceptions if caller is not a script? LOGGER.info("{} Exception: {}", debugScriptName, ex); LEGACY_CALL_LOGGER.debug("{} Exception: {}", debugScriptName, ex); throw new ScriptException("Failed to execute script string: " + ex.getMessage(), ex); } finally { Context.exit(); ValueConverter.GLOBAL_CONVERTER.set(previousConverter); final long endTime = System.currentTimeMillis(); LOGGER.info("{} End {} ms", debugScriptName, Long.valueOf(endTime - startTime)); LEGACY_CALL_LOGGER.debug("{} End {} ms", debugScriptName, Long.valueOf(endTime - startTime)); } } /** * {@inheritDoc} */ @Override public Object executeInScope(final ScriptLocation location, final Object scope) { ParameterCheck.mandatory("location", location); final ReferenceScript actualScript = new ScriptLocationAdapter(location); final Script script = this.getCompiledScript(actualScript); final String debugScriptName; { final String path = location.getPath(); final int i = path.lastIndexOf('/'); debugScriptName = i != -1 ? path.substring(i + 1) : path; } LOGGER.info("{} Start", debugScriptName); LEGACY_CALL_LOGGER.debug("{} Start", debugScriptName); final long startTime = System.currentTimeMillis(); final ValueConverter previousConverter = ValueConverter.GLOBAL_CONVERTER.get(); ValueConverter.GLOBAL_CONVERTER.set(this.valueConverter); final Context cx = Context.enter(); try { final DelegatingWrapFactory wrapFactory = new DelegatingWrapFactory(); cx.setWrapFactory(wrapFactory); List<ReferenceScript> currentChain = this.activeScriptLocationChain.get(cx); boolean newChain = false; if (currentChain == null) { this.updateLocationChainsBeforeExceution(cx); currentChain = this.activeScriptLocationChain.get(cx); newChain = true; } // else: assume the original script chain is continued currentChain.add(actualScript); try { final Scriptable realScope; if (scope == null) { if (this.shareScopes) { final Scriptable sharedScope = actualScript.isSecure() ? this.unrestrictedShareableScope : this.restrictedShareableScope; realScope = cx.newObject(sharedScope); realScope.setPrototype(sharedScope); realScope.setParentScope(null); } else { realScope = this.setupScope(cx, actualScript.isSecure(), false); } } else if (!(scope instanceof Scriptable)) { realScope = new NativeJavaObject(null, scope, scope.getClass()); if (this.shareScopes) { final Scriptable sharedScope = actualScript.isSecure() ? this.unrestrictedShareableScope : this.restrictedShareableScope; realScope.setPrototype(sharedScope); } else { final Scriptable baseScope = this.setupScope(cx, actualScript.isSecure(), false); realScope.setPrototype(baseScope); } } else { realScope = (Scriptable) scope; } wrapFactory.setScope(realScope); final Object result = this.executeScriptInScopeImpl(script, realScope); return result; } finally { currentChain.remove(currentChain.size() - 1); if (newChain) { this.updateLocationChainsAfterReturning(cx); } } } catch (final Exception ex) { // TODO: error handling / bubbling to caller? how to handle Rhino exceptions if caller is not a script? LOGGER.info("{} Exception: {}", debugScriptName, ex); LEGACY_CALL_LOGGER.debug("{} Exception: {}", debugScriptName, ex); throw new ScriptException("Failed to execute script '" + location.toString() + "': " + ex.getMessage(), ex); } finally { Context.exit(); ValueConverter.GLOBAL_CONVERTER.set(previousConverter); final long endTime = System.currentTimeMillis(); LOGGER.info("{} End {} ms", debugScriptName, Long.valueOf(endTime - startTime)); LEGACY_CALL_LOGGER.debug("{} End {} ms", debugScriptName, Long.valueOf(endTime - startTime)); } } /** * {@inheritDoc} */ @Override public Object initializeScope(final ScriptLocation location) { ParameterCheck.mandatory("location", location); final Scriptable scope; final ValueConverter previousConverter = ValueConverter.GLOBAL_CONVERTER.get(); ValueConverter.GLOBAL_CONVERTER.set(this.valueConverter); final Context cx = Context.enter(); try { cx.setWrapFactory(new DelegatingWrapFactory()); final boolean secureScript = location.isSecure(); if (this.shareScopes) { final Scriptable sharedScope = secureScript ? this.unrestrictedShareableScope : this.restrictedShareableScope; scope = cx.newObject(sharedScope); scope.setPrototype(sharedScope); scope.setParentScope(null); } else { scope = this.setupScope(cx, secureScript, false); } } finally { Context.exit(); ValueConverter.GLOBAL_CONVERTER.set(previousConverter); } return scope; } /** * {@inheritDoc} */ @Override public ReferenceScript getContextScriptLocation() { final List<ReferenceScript> currentChain = this.activeScriptLocationChain.get(Context.getCurrentContext()); final ReferenceScript result; if (currentChain != null && !currentChain.isEmpty()) { result = currentChain.get(currentChain.size() - 1); } else { result = null; } return result; } /** * * {@inheritDoc} */ @Override public List<ReferenceScript> getScriptCallChain() { final List<ReferenceScript> currentChain = this.activeScriptLocationChain.get(Context.getCurrentContext()); final List<ReferenceScript> result; if (currentChain != null) { result = new ArrayList<ReferenceScript>(currentChain); } else { result = null; } return result; } /** * * {@inheritDoc} */ @Override public void inheritCallChain(final Object parentContext) { ParameterCheck.mandatory("parentContext", parentContext); final Context currentContext = Context.getCurrentContext(); List<ReferenceScript> activeChain = this.activeScriptLocationChain.get(currentContext); if (activeChain != null) { throw new IllegalStateException("Context call chain has already been initialized"); } final List<ReferenceScript> parentChain = this.activeScriptLocationChain.get(parentContext); if (parentChain == null) { throw new IllegalArgumentException("Parent context has no call chain associated with it"); } activeChain = new ArrayList<ReferenceScript>(parentChain); this.activeScriptLocationChain.put(currentContext, activeChain); } /** * * {@inheritDoc} */ @Override public void debuggerAttached() { this.debuggerActive = true; } /** * * {@inheritDoc} */ @Override public void debuggerDetached() { this.debuggerActive = false; } /** * * {@inheritDoc} */ @Override public void registerScopeContributor(final ScopeContributor contributor) { if (contributor != null) { // can use synchronized here since scope creation / registration should not occur that often in a relevant // production scenario // (when immutable scopes are shared) synchronized (this.registeredContributors) { this.registeredContributors.add(contributor); } } } /** * {@inheritDoc} */ @Override public void reset() { this.scriptCacheLock.writeLock().lock(); try { this.scriptCache.clear(); } finally { this.scriptCacheLock.writeLock().unlock(); } this.dynamicScriptCacheLock.writeLock().lock(); try { this.dynamicScriptCache.clear(); } finally { this.dynamicScriptCacheLock.writeLock().unlock(); } } /** * @param valueConverter * the valueConverter to set */ public final void setValueConverter(final ValueConverter valueConverter) { this.valueConverter = valueConverter; } /** * @param shareScopes * the shareScopes to set */ public final void setShareScopes(final boolean shareScopes) { this.shareScopes = shareScopes; } /** * @param compileScripts * the compileScripts to set */ public final void setCompileScripts(final boolean compileScripts) { this.compileScripts = compileScripts; } /** * @param optimizationLevel * the optimizaionLevel to set */ public final void setOptimizationLevel(final int optimizationLevel) { if (!Context.isValidOptimizationLevel(optimizationLevel)) { throw new IllegalArgumentException("Invalid optimization level: " + optimizationLevel); } this.optimizationLevel = optimizationLevel; } /** * @param failoverToLessOptimization * the failoverToLessOptimization to set */ public final void setFailoverToLessOptimization(final boolean failoverToLessOptimization) { this.failoverToLessOptimization = failoverToLessOptimization; } protected ReferenceScript toReferenceScript(final String source) { try { final MD5 md5 = new MD5(); final String digest = md5.digest(source.getBytes("UTF-8")); final String scriptName = MessageFormat.format("string:///DynamicJS-{0}.js", digest); final ReferenceScript script = new ReferenceScript.DynamicScript(scriptName, source); return script; } catch (final UnsupportedEncodingException err) { throw new ScriptException("Failed process supplied script", err); } } protected void updateLocationChainsBeforeExceution(final Context currentContext) { final List<ReferenceScript> activeChain = this.activeScriptLocationChain.get(currentContext); if (activeChain != null) { List<List<ReferenceScript>> recursionChains = this.recursionScriptLocationChains.get(currentContext); if (recursionChains == null) { recursionChains = new LinkedList<List<ReferenceScript>>(); this.recursionScriptLocationChains.put(currentContext, recursionChains); } recursionChains.add(0, activeChain); } this.activeScriptLocationChain.put(currentContext, new LinkedList<ReferenceScript>()); } protected void updateLocationChainsAfterReturning(final Context currentContext) { this.activeScriptLocationChain.remove(currentContext); final List<List<ReferenceScript>> recursionChains = this.recursionScriptLocationChains.get(currentContext); if (recursionChains != null) { final List<ReferenceScript> previousChain = recursionChains.remove(0); if (recursionChains.isEmpty()) { this.recursionScriptLocationChains.remove(currentContext); } this.activeScriptLocationChain.put(currentContext, previousChain); } } protected Script getCompiledScript(final ReferenceScript location) { Script script = null; String realPath = null; final Collection<ReferencePathType> supportedReferencePathTypes = location.getSupportedReferencePathTypes(); for (final ReferencePathType pathType : REAL_PATH_SUCCESSION) { if (realPath == null && supportedReferencePathTypes.contains(pathType)) { realPath = location.getReferencePath(pathType); } } final String classPath = location.getReferencePath(CommonReferencePath.CLASSPATH); if (realPath == null) { final String path = location instanceof ScriptLocationAdapter ? ((ScriptLocationAdapter) location).getPath() : location.getFullName(); // check if the path is in classpath form // TODO: can we generalize external form file:// to a classpath-relative location? (best-effort) if (!path.matches("^(classpath[*]?:).*$") && (classPath == null || !path.equals(classPath))) { // take path as is - can be anything depending on how content is loaded realPath = path; } else { // we always want to have a fully-qualified file-protocol path (unless we can generalize all to classpath-relative // locations) final String resourcePath; if (classPath != null && classPath.equals(path)) { resourcePath = classPath; } else { resourcePath = path.substring(path.indexOf(':') + 1); } URL resource = this.getClass().getClassLoader().getResource(resourcePath); if (resource == null && resourcePath.startsWith("/")) { resource = this.getClass().getClassLoader().getResource(resourcePath.substring(1)); } if (resource != null) { realPath = resource.toExternalForm(); } else { // should not occur in normal circumstances, but since ScriptLocation can be anything... realPath = path; } } } // store since it may be reset between cache-check and cache-put, and we don't want debug-enabled scripts cached final boolean debuggerActive = this.debuggerActive; final boolean dynamicScript = location instanceof DynamicScript; // test the cache for a pre-compiled script matching our path if (this.compileScripts && !debuggerActive && (dynamicScript || location.isCachable())) { script = this.lookupScriptCache(dynamicScript ? this.dynamicScriptCache : this.scriptCache, dynamicScript ? this.dynamicScriptCacheLock : this.scriptCacheLock, realPath); } if (script == null) { LOGGER.debug("Resolving and compiling script path: {}", realPath); try { final ByteArrayOutputStream os = new ByteArrayOutputStream(); FileCopyUtils.copy(location.getInputStream(), os); // both streams are closed final byte[] bytes = os.toByteArray(); final String source = new String(bytes, "UTF-8"); script = this.getCompiledScript(source, realPath); } catch (final IOException err) { LOGGER.error("Failed to compile supplied script", err); throw new ScriptException("Failed to compile supplied script: " + err.getMessage(), err); } if (this.compileScripts && !debuggerActive && (dynamicScript || location.isCachable())) { this.updateScriptCache(dynamicScript ? this.dynamicScriptCache : this.scriptCache, dynamicScript ? this.dynamicScriptCacheLock : this.scriptCacheLock, realPath, script); } LOGGER.debug("Compiled script for {}", realPath); } else { LOGGER.debug("Using previously compiled script for {}", realPath); } return script; } protected Script getCompiledScript(final String source, final String path) { ParameterCheck.mandatoryString("path", path); // only mandatory - may be empty string as NO-OP script ParameterCheck.mandatory("source", source); try { final Script script; final String resolvedSource = this.resolveScriptImports(source); final Context cx = Context.enter(); try { if (this.compileScripts && !this.debuggerActive) { cx.setGeneratingDebug(true); cx.setGeneratingSource(true); int optimizationLevel = this.optimizationLevel; Script bestEffortOptimizedScript = null; while (optimizationLevel >= -1 && bestEffortOptimizedScript == null) { try { cx.setOptimizationLevel(optimizationLevel--); bestEffortOptimizedScript = cx.compileString(resolvedSource, path, 1, null); } catch (final RuntimeException ex) { // unfortunately, all exceptions emitted from compilation are RuntimeExceptions // but at the least, they are RuntimeException specifically if (!this.failoverToLessOptimization || !ex.getClass().isAssignableFrom(RuntimeException.class)) { // if failover is not to be attempted or exception is a specialized RuntimeException throw ex; } else if (optimizationLevel > -1) { // we do at least log LOGGER.info( "Compilation failed of {} failed with runtime exception {} - attempting lower optimization level", path, ex.getMessage()); } else { // we do at least log LOGGER.info( "Compilation failed of {} failed with runtime exception {} - no further attempt", path, ex.getMessage()); } } } script = bestEffortOptimizedScript; } else { cx.setOptimizationLevel(-1); script = cx.compileString(resolvedSource, path, 1, null); } } finally { Context.exit(); } return script; } catch (final Exception ex) { // we don't know the sorts of exceptions that can come from Rhino, so handle any and all exceptions LOGGER.error("Failed to compile supplied script", ex); throw new ScriptException("Failed to compile supplied script: " + ex.getMessage(), ex); } } /** * Resolves the import directives in the specified script to proper import API calls. Supported import directives are of the following * form: * * <pre> * <import resource="classpath:alfresco/includeme.js"> * <import resource="workspace://SpacesStore/6f73de1b-d3b4-11db-80cb-112e6c2ea048"> * <import resource="/Company Home/Data Dictionary/Scripts/includeme.js"> * </pre> * * Either a classpath resource, NodeRef or cm:name path based script can be imported. * * @param script * The script content to resolve imports in * * @return a valid script with all includes resolved to a proper import API call */ @SuppressWarnings("static-method") protected String resolveScriptImports(final String script) { final String classpathResolvedScript = script.replaceAll(CLASSPATH_RESOURCE_IMPORT_PATTERN, CLASSPATH_RESOURCE_IMPORT_REPLACEMENT); final String nodeRefResolvedScript = classpathResolvedScript.replaceAll(NODE_REF_RESOURCE_IMPORT_PATTERN, NODE_REF_RESOURCE_IMPORT_REPLACEMENT); final String legacyNamePathResolvedScript = nodeRefResolvedScript .replaceAll(LEGACY_NAME_PATH_RESOURCE_IMPORT_PATTERN, LEGACY_NAME_PATH_RESOURCE_IMPORT_REPLACEMENT); return legacyNamePathResolvedScript; } protected Script lookupScriptCache(final Map<String, Script> cache, final ReadWriteLock lock, final String key) { Script script; lock.readLock().lock(); try { script = cache.get(key); } finally { lock.readLock().unlock(); } return script; } protected void updateScriptCache(final Map<String, Script> cache, final ReadWriteLock lock, final String key, final Script script) { lock.writeLock().lock(); try { cache.put(key, script); if (cache.size() > this.maxScriptCacheSize) { final Iterator<String> keyIterator = cache.keySet().iterator(); while (cache.size() > this.maxScriptCacheSize) { final String keyToRemove = keyIterator.next(); cache.remove(keyToRemove); } } } finally { lock.writeLock().unlock(); } } protected Object executeScriptImpl(final Script script, final Map<String, Object> model, final boolean secureScript, final String debugScriptName) throws AlfrescoRuntimeException { final long startTime = System.currentTimeMillis(); LOGGER.info("{} Start", debugScriptName); LEGACY_CALL_LOGGER.debug("{} Start", debugScriptName); final ValueConverter previousConverter = ValueConverter.GLOBAL_CONVERTER.get(); ValueConverter.GLOBAL_CONVERTER.set(this.valueConverter); final Context cx = Context.enter(); try { final DelegatingWrapFactory wrapFactory = new DelegatingWrapFactory(); cx.setWrapFactory(wrapFactory); final Scriptable scope; if (this.shareScopes) { final Scriptable sharedScope = secureScript ? this.unrestrictedShareableScope : this.restrictedShareableScope; scope = cx.newObject(sharedScope); scope.setPrototype(sharedScope); scope.setParentScope(null); } else { scope = this.setupScope(cx, secureScript, false); } wrapFactory.setScope(scope); // insert supplied object model into root of the default scope for (final String key : model.keySet()) { final Object obj = model.get(key); // set the root scope on appropriate objects // this is used to allow native JS object creation etc. if (obj instanceof Scopeable) { ((Scopeable) obj).setScope(scope); } // convert/wrap each object to JavaScript compatible final Object jsObject = this.valueConverter.convertValueForScript(obj); // repeat on resulting object (may have been converted into Scopeable) if (jsObject instanceof Scopeable) { ((Scopeable) jsObject).setScope(scope); } // insert into the root scope ready for access by the script ScriptableObject.putProperty(scope, key, jsObject); } // execute the script and return the result final Object scriptResult = this.executeScriptInScopeImpl(script, scope); return scriptResult; } catch (final WrappedException w) { LOGGER.info("{} Exception: {}", debugScriptName, w); LEGACY_CALL_LOGGER.debug("{} Exception: {}", debugScriptName, w); final Throwable err = w.getWrappedException(); throw new ScriptException(w.getMessage(), err); } catch (final Exception ex) { LOGGER.info("{} Exception: {}", debugScriptName, ex); LEGACY_CALL_LOGGER.debug("{} Exception: {}", debugScriptName, ex); throw new ScriptException(ex.getMessage(), ex); } finally { Context.exit(); ValueConverter.GLOBAL_CONVERTER.set(previousConverter); final long endTime = System.currentTimeMillis(); LOGGER.info("{} End {} ms", debugScriptName, Long.valueOf(endTime - startTime)); LEGACY_CALL_LOGGER.debug("{} End {} ms", debugScriptName, Long.valueOf(endTime - startTime)); } } protected Object executeScriptInScopeImpl(final Script script, final Scriptable scope) { final Context cx = Context.enter(); try { cx.setLocale(I18NUtil.getLocale()); if (this.compileScripts) { cx.setOptimizationLevel(9); } // make sure scripts always have the relevant processor extensions available for (final ProcessorExtension ex : this.processorExtensions.values()) { if (!ScriptableObject.hasProperty(scope, ex.getExtensionName())) { if (ex instanceof Scopeable) { ((Scopeable) ex).setScope(scope); } // convert/wrap each to JavaScript compatible final Object jsObject = this.valueConverter.convertValueForScript(ex); if (jsObject instanceof Scopeable) { ((Scopeable) jsObject).setScope(scope); } // insert into the scope ready for access by the script ScriptableObject.putProperty(scope, ex.getExtensionName(), jsObject); } } final Object scriptResult = script.exec(cx, scope); // extract java object result if wrapped by Rhino final Object result = this.valueConverter.convertValueForJava(scriptResult); return result; } finally { Context.exit(); } } protected Scriptable setupScope(final Context executionContext, final boolean trustworthyScript, final boolean mutableScope) { final Scriptable scope; if (trustworthyScript) { // allow access to all libraries and objects scope = new ImporterTopLevel(executionContext, !mutableScope); } else { scope = executionContext.initStandardObjects(null, !mutableScope); // Remove reflection / Java-interactivity capabilities scope.delete("Packages"); scope.delete("getClass"); scope.delete("java"); } synchronized (this.registeredContributors) { for (final ScopeContributor contributor : this.registeredContributors) { contributor.contributeToScope(scope, trustworthyScript, mutableScope); } } return scope; } }