diff --git a/config/alfresco/script-services-context.xml b/config/alfresco/script-services-context.xml index 97582de210..e6498fc275 100644 --- a/config/alfresco/script-services-context.xml +++ b/config/alfresco/script-services-context.xml @@ -7,6 +7,12 @@ + + ${spaces.store} + + + ${spaces.company_home.childname} + diff --git a/source/java/org/alfresco/repo/jscript/RhinoScriptService.java b/source/java/org/alfresco/repo/jscript/RhinoScriptService.java index 4d948272c3..052edc251c 100644 --- a/source/java/org/alfresco/repo/jscript/RhinoScriptService.java +++ b/source/java/org/alfresco/repo/jscript/RhinoScriptService.java @@ -24,37 +24,38 @@ */ package org.alfresco.repo.jscript; +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.Reader; -import java.io.StringReader; import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.StringTokenizer; import org.alfresco.error.AlfrescoRuntimeException; import org.alfresco.model.ContentModel; import org.alfresco.service.ServiceRegistry; +import org.alfresco.service.cmr.model.FileInfo; +import org.alfresco.service.cmr.model.FileNotFoundException; +import org.alfresco.service.cmr.repository.ContentIOException; import org.alfresco.service.cmr.repository.ContentReader; import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.cmr.repository.ScriptException; import org.alfresco.service.cmr.repository.ScriptImplementation; import org.alfresco.service.cmr.repository.ScriptLocation; -import org.alfresco.service.cmr.repository.ScriptImplementation; import org.alfresco.service.cmr.repository.ScriptService; +import org.alfresco.service.cmr.repository.StoreRef; import org.alfresco.service.namespace.QName; import org.alfresco.util.ParameterCheck; import org.apache.log4j.Logger; import org.mozilla.javascript.Context; -import org.mozilla.javascript.EvaluatorException; import org.mozilla.javascript.NativeArray; import org.mozilla.javascript.Scriptable; import org.mozilla.javascript.ScriptableObject; import org.mozilla.javascript.Wrapper; +import org.springframework.util.FileCopyUtils; /** * Implementation of the ScriptService using the Rhino JavaScript engine. @@ -65,9 +66,20 @@ public class RhinoScriptService implements ScriptService { private static final Logger logger = Logger.getLogger(RhinoScriptService.class); + private static final String IMPORT_PREFIX = " globalScripts = new ArrayList(); @@ -81,6 +93,24 @@ public class RhinoScriptService implements ScriptService this.services = services; } + /** + * Set the default store reference + * + * @param storeRef The default store reference + */ + public void setStoreUrl(String storeRef) + { + this.storeRef = new StoreRef(storeRef); + } + + /** + * @param storePath The store path to set. + */ + public void setStorePath(String storePath) + { + this.storePath = storePath; + } + /** * @see org.alfresco.service.cmr.repository.ScriptService#registerScript(java.lang.Object) */ @@ -105,7 +135,6 @@ public class RhinoScriptService implements ScriptService logger.debug("Executing script: " + scriptClasspath); } - Reader reader = null; try { InputStream stream = getClass().getClassLoader().getResourceAsStream(scriptClasspath); @@ -113,21 +142,16 @@ public class RhinoScriptService implements ScriptService { throw new AlfrescoRuntimeException("Unable to load classpath resource: " + scriptClasspath); } - reader = new InputStreamReader(stream); + ByteArrayOutputStream os = new ByteArrayOutputStream(); + FileCopyUtils.copy(stream, os); // both streams are closed + byte[] bytes = os.toByteArray(); - return executeScriptImpl(reader, model); + return executeScriptImpl(resolveScriptImports(new String(bytes)), model); } catch (Throwable err) { throw new ScriptException("Failed to execute script '" + scriptClasspath + "': " + err.getMessage(), err); } - finally - { - if (reader != null) - { - try {reader.close();} catch (IOException ioErr) {} - } - } } /** @@ -146,7 +170,6 @@ public class RhinoScriptService implements ScriptService logger.debug("Executing script: " + scriptRef.toString()); } - Reader reader = null; try { if (this.services.getNodeService().exists(scriptRef) == false) @@ -163,21 +186,13 @@ public class RhinoScriptService implements ScriptService { throw new AlfrescoRuntimeException("Script Node content not found: " + scriptRef); } - reader = new InputStreamReader(cr.getContentInputStream()); - return executeScriptImpl(reader, model); + return executeScriptImpl(resolveScriptImports(cr.getContentString()), model); } catch (Throwable err) { throw new ScriptException("Failed to execute script '" + scriptRef.toString() + "': " + err.getMessage(), err); } - finally - { - if (reader != null) - { - try {reader.close();} catch (IOException ioErr) {} - } - } } /** @@ -193,22 +208,18 @@ public class RhinoScriptService implements ScriptService logger.debug("Executing script: " + location.toString()); } - Reader reader = null; try { - return executeScriptImpl(location.getReader(), model); + ByteArrayOutputStream os = new ByteArrayOutputStream(); + FileCopyUtils.copy(location.getInputStream(), os); // both streams are closed + byte[] bytes = os.toByteArray(); + // create the script string from the byte[] + return executeScriptImpl(resolveScriptImports(new String(bytes)), model); } catch (Throwable err) { throw new ScriptException("Failed to execute script '" + location.toString() + "': " + err.getMessage(), err); } - finally - { - if (reader != null) - { - try {reader.close();} catch (IOException ioErr) {} - } - } } /** @@ -227,12 +238,9 @@ public class RhinoScriptService implements ScriptService logger.debug("Executing script:\n" + script); } - Reader reader = null; try { - reader = new StringReader(script); - - return executeScriptImpl(reader, model); + return executeScriptImpl(resolveScriptImports(script), model); } catch (Throwable err) { @@ -241,17 +249,274 @@ public class RhinoScriptService implements ScriptService } /** - * Execute the script content from the supplied Reader. Adds the data model into the default - * root scope for access by the script. + * Resolve the imports in the specified script. Include directives are of the following form: + *
+     * 
+     * 
+     * 
+     * 
+ * Either a classpath resource, NodeRef or cm:name path based script can be includes. Multiple includes + * of the same script are dealt with correctly and nested includes of scripts is fully supported. + *

+ * Note that for performance reasons the script import directive syntax and placement in the file + * is very strict. The import lines must always be first in the file - even before any comments. + * Immediately that the script service detects a non-import line it will assume the rest of the + * file is executable script and no longer attempt to search for any further import directives. Therefore + * all imports should be at the top of the script, one following the other, in the correct syntax and with + * no comments present - the only separators valid between import directives is white space. * - * @param reader Reader referencing the script to execute. + * @param script The script content to resolve imports in + * + * @return a valid script with all nested includes resolved into a single script instance + */ + private String resolveScriptImports(String script) + { + // use a linked hashmap to preserve order of includes - the key in the collection is used + // to resolve multiple includes of the same scripts and therefore cyclic includes also + Map scriptlets = new LinkedHashMap(8, 1.0f); + + // perform a recursive resolve of all script imports + recurseScriptImports(SCRIPT_ROOT, script, scriptlets); + + if (scriptlets.size() == 1) + { + // quick exit for single script with no includes + if (logger.isDebugEnabled()) + logger.debug("Script content resolved to:\r\n" + script); + + return script; + } + else + { + // calculate total size of buffer required for the script and all includes + int length = 0; + for (String scriptlet : scriptlets.values()) + { + length += scriptlet.length(); + } + // append the scripts together to make a single script + StringBuilder result = new StringBuilder(length); + for (String scriptlet : scriptlets.values()) + { + result.append(scriptlet); + } + + if (logger.isDebugEnabled()) + logger.debug("Script content resolved to:\r\n" + result.toString()); + + return result.toString(); + } + } + + /** + * Recursively resolve imports in the specified scripts, adding the imports to the + * specific list of scriplets to combine later. + * + * @param location Script location - used to ensure duplicates are not added + * @param script The script to recursively resolve imports for + * @param scripts The collection of scriplets to execute with imports resolved and removed + */ + private void recurseScriptImports(String location, String script, Map scripts) + { + int index = 0; + // skip any initial whitespace + for (; index') + { + // found end of import line - so we have a resource path + String resource = script.substring(resourceStart, index); + + if (logger.isDebugEnabled()) + logger.debug("Found script resource import: " + resource); + + if (scripts.containsKey(resource) == false) + { + // load the script resource (and parse any recursive includes...) + String includedScript = loadScriptResource(resource); + if (includedScript != null) + { + if (logger.isDebugEnabled()) + logger.debug("Succesfully located script '" + resource + "'"); + recurseScriptImports(resource, includedScript, scripts); + } + } + else + { + if (logger.isDebugEnabled()) + logger.debug("Note: already imported resource: " + resource); + } + + // continue scanning this script for additional includes + // skip the last two characters of the import directive + for (index += 2; index"); + } + else + { + throw new ScriptException( + "Malformed 'import' line - must be first in file, no comments and strictly of the form:" + + "\r\n"); + } + } + else + { + // no (further) includes found - include the original script content + if (logger.isDebugEnabled()) + logger.debug("Imports resolved, adding resource '" + location + "' content:\r\n" + script); + scripts.put(location, script); + } + } + + /** + * Load a script content from the specific resource path. + * + * @param resource Resources can be of the form: + *

+     * classpath:alfresco/includeme.js
+     * workspace://SpacesStore/6f73de1b-d3b4-11db-80cb-112e6c2ea048
+     * /Company Home/Data Dictionary/Scripts/includeme.js
+     * 
+ * + * @return the content from the resource, null if not recognised format + * + * @throws AlfrescoRuntimeException on any IO or ContentIO error + */ + private String loadScriptResource(String resource) + { + String result = null; + + if (resource.startsWith(PATH_CLASSPATH)) + { + try + { + // load from classpath + String scriptClasspath = resource.substring(PATH_CLASSPATH.length()); + InputStream stream = getClass().getClassLoader().getResourceAsStream(scriptClasspath); + if (stream == null) + { + throw new AlfrescoRuntimeException("Unable to load included script classpath resource: " + resource); + } + ByteArrayOutputStream os = new ByteArrayOutputStream(); + FileCopyUtils.copy(stream, os); // both streams are closed + byte[] bytes = os.toByteArray(); + // create the string from the byte[] using encoding if necessary + result = new String(bytes); + } + catch (IOException err) + { + throw new AlfrescoRuntimeException("Unable to load included script classpath resource: " + resource); + } + } + else + { + NodeRef scriptRef; + if (resource.startsWith("/")) + { + // resolve from default SpacesStore as cm:name based path + // TODO: remove this once FFS correctly allows name path resolving from store root! + NodeRef rootNodeRef = this.services.getNodeService().getRootNode(this.storeRef); + List nodes = this.services.getSearchService().selectNodes( + rootNodeRef, this.storePath, null, this.services.getNamespaceService(), false); + if (nodes.size() == 0) + { + throw new AlfrescoRuntimeException("Unable to find store path: " + this.storePath); + } + StringTokenizer tokenizer = new StringTokenizer(resource, "/"); + List elements = new ArrayList(6); + if (tokenizer.hasMoreTokens()) + { + tokenizer.nextToken(); + } + while (tokenizer.hasMoreTokens()) + { + elements.add(tokenizer.nextToken()); + } + try + { + FileInfo fileInfo = this.services.getFileFolderService().resolveNamePath(nodes.get(0), elements); + scriptRef = fileInfo.getNodeRef(); + } + catch (FileNotFoundException err) + { + throw new AlfrescoRuntimeException("Unable to load included script repository resource: " + resource); + } + } + else + { + scriptRef = new NodeRef(resource); + } + + // load from NodeRef default content property + try + { + ContentReader cr = this.services.getContentService().getReader(scriptRef, ContentModel.PROP_CONTENT); + if (cr == null || cr.exists() == false) + { + throw new AlfrescoRuntimeException("Included Script Node content not found: " + resource); + } + result = cr.getContentString(); + } + catch (ContentIOException err) + { + throw new AlfrescoRuntimeException("Unable to load included script repository resource: " + resource); + } + } + + return result; + } + + /** + * Execute the supplied script content. Adds the default data model and custom configured root + * objects into the root scope for access by the script. + * + * @param script The script to execute. * @param model Data model containing objects to be added to the root scope. * * @return result of the script execution, can be null. * * @throws AlfrescoRuntimeException */ - private Object executeScriptImpl(Reader reader, Map model) + private Object executeScriptImpl(String script, Map model) throws AlfrescoRuntimeException { long startTime = 0; @@ -276,9 +541,9 @@ public class RhinoScriptService implements ScriptService } // add the global scripts - for (ScriptImplementation script : this.globalScripts) + for (ScriptImplementation ex : this.globalScripts) { - model.put(script.getScriptName(), script); + model.put(ex.getScriptName(), script); } // insert supplied object model into root of the default scope @@ -300,9 +565,9 @@ public class RhinoScriptService implements ScriptService } // execute the script - Object result = cx.evaluateReader(scope, reader, "AlfrescoScript", 1, null); + Object result = cx.evaluateString(scope, script, "AlfrescoScript", 1, null); - // extract java object result if wrapped by rhinoscript + // extract java object result if wrapped by Rhino if (result instanceof Wrapper) { result = ((Wrapper)result).unwrap();