();
@@ -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();