/* * Copyright (C) 2005-2007 Alfresco Software Limited. * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * This program 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 General Public License for more details. * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * As a special exception to the terms and conditions of version 2.0 of * the GPL, you may redistribute this Program in connection with Free/Libre * and Open Source Software ("FLOSS") applications as described in Alfresco's * FLOSS exception. You should have recieved a copy of the text describing * the FLOSS exception, and it is also available here: * http://www.alfresco.com/legal/licensing" */ package org.alfresco.repo.jscript; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; 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.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.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. * * @author Kevin Roast */ public class RhinoScriptService implements ScriptService { private static final Logger logger = Logger.getLogger(RhinoScriptService.class); private static final String IMPORT_PREFIX = " globalScripts = new ArrayList(); /** * Set the Service Registry * * @param service registry */ public void setServiceRegistry(ServiceRegistry services) { 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) */ public void registerScript(ScriptImplementation script) { this.globalScripts.add(script); } /** * @see org.alfresco.service.cmr.repository.ScriptService#executeScript(java.lang.String, java.util.Map) */ public Object executeScript(String scriptClasspath, Map model) throws ScriptException { if (scriptClasspath == null) { throw new IllegalArgumentException("Script ClassPath is mandatory."); } if (logger.isDebugEnabled()) { logger.debug("Executing script: " + scriptClasspath); } try { InputStream stream = getClass().getClassLoader().getResourceAsStream(scriptClasspath); if (stream == null) { throw new AlfrescoRuntimeException("Unable to load classpath resource: " + scriptClasspath); } ByteArrayOutputStream os = new ByteArrayOutputStream(); FileCopyUtils.copy(stream, os); // both streams are closed byte[] bytes = os.toByteArray(); return executeScriptImpl(resolveScriptImports(new String(bytes)), model); } catch (Throwable err) { throw new ScriptException("Failed to execute script '" + scriptClasspath + "': " + err.getMessage(), err); } } /** * @see org.alfresco.service.cmr.repository.ScriptService#executeScript(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.namespace.QName, java.util.Map) */ public Object executeScript(NodeRef scriptRef, QName contentProp, Map model) throws ScriptException { if (scriptRef == null) { throw new IllegalArgumentException("Script NodeRef is mandatory."); } if (logger.isDebugEnabled()) { logger.debug("Executing script: " + scriptRef.toString()); } try { if (this.services.getNodeService().exists(scriptRef) == false) { throw new AlfrescoRuntimeException("Script Node does not exist: " + scriptRef); } if (contentProp == null) { contentProp = ContentModel.PROP_CONTENT; } ContentReader cr = this.services.getContentService().getReader(scriptRef, contentProp); if (cr == null || cr.exists() == false) { throw new AlfrescoRuntimeException("Script Node content not found: " + scriptRef); } return executeScriptImpl(resolveScriptImports(cr.getContentString()), model); } catch (Throwable err) { throw new ScriptException("Failed to execute script '" + scriptRef.toString() + "': " + err.getMessage(), err); } } /** * @see org.alfresco.service.cmr.repository.ScriptService#executeScript(org.alfresco.service.cmr.repository.ScriptLocation, java.util.Map) */ public Object executeScript(ScriptLocation location, Map model) throws ScriptException { ParameterCheck.mandatory("Location", location); if (logger.isDebugEnabled()) { logger.debug("Executing script: " + location.toString()); } try { 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); } } /** * @see org.alfresco.service.cmr.repository.ScriptService#executeScriptString(java.lang.String, java.util.Map) */ public Object executeScriptString(String script, Map model) throws ScriptException { if (script == null || script.length() == 0) { throw new IllegalArgumentException("Script argument is mandatory."); } if (logger.isDebugEnabled()) { logger.debug("Executing script:\n" + script); } try { return executeScriptImpl(resolveScriptImports(script), model); } catch (Throwable err) { throw new ScriptException("Failed to execute supplied script: " + err.getMessage(), err); } } /** * 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 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 // we have to assume "/Company Home" as the root for now 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(String script, Map model) throws AlfrescoRuntimeException { long startTime = 0; if (logger.isDebugEnabled()) { startTime = System.currentTimeMillis(); } // check that rhino script engine is available Context cx = Context.enter(); try { // The easiest way to embed Rhino is just to create a new scope this way whenever // you need one. However, initStandardObjects is an expensive method to call and it // allocates a fair amount of memory. Scriptable scope = cx.initStandardObjects(); // there's always a model, if only to hold the util objects if (model == null) { model = new HashMap(); } // add the global scripts for (ScriptImplementation ex : this.globalScripts) { model.put(ex.getScriptName(), ex); } // insert supplied object model into root of the default scope for (String key : model.keySet()) { // set the root scope on appropriate objects // this is used to allow native JS object creation etc. Object obj = model.get(key); if (obj instanceof Scopeable) { ((Scopeable)obj).setScope(scope); } // convert/wrap each object to JavaScript compatible Object jsObject = Context.javaToJS(obj, scope); // insert into the root scope ready for access by the script ScriptableObject.putProperty(scope, key, jsObject); } // execute the script Object result = cx.evaluateString(scope, script, "AlfrescoScript", 1, null); // extract java object result if wrapped by Rhino if (result instanceof Wrapper) { result = ((Wrapper)result).unwrap(); } else if (result instanceof NativeArray) { result = Context.jsToJava(result, Object[].class); } return result; } catch (Throwable err) { throw new AlfrescoRuntimeException(err.getMessage(), err); } finally { Context.exit(); if (logger.isDebugEnabled()) { long endTime = System.currentTimeMillis(); logger.debug("Time to execute script: " + (endTime - startTime) + "ms"); } } } /** * Create the default data-model available to scripts as global scope level objects: *

* 'companyhome' - the Company Home node
* 'userhome' - the current user home space node
* 'person' - the node representing the current user Person
* 'script' - the node representing the script itself (may not be available)
* 'document' - document context node (may not be available)
* 'space' - space context node (may not be available) * * @param services ServiceRegistry * @param person The current user Person Node * @param companyHome The CompanyHome ref * @param userHome The User home space ref * @param script Optional ref to the script itself * @param document Optional ref to a document Node * @param space Optional ref to a space Node * @param resolver Image resolver to resolve icon images etc. * * @return A Map of global scope scriptable Node objects */ public static Map buildDefaultModel( ServiceRegistry services, NodeRef person, NodeRef companyHome, NodeRef userHome, NodeRef script, NodeRef document, NodeRef space) { Map model = new HashMap(); // add the well known node wrapper objects model.put("companyhome", new Node(companyHome, services)); model.put("userhome", new Node(userHome, services)); model.put("person", new Node(person, services)); if (script != null) { model.put("script", new Node(script, services)); } if (document != null) { model.put("document", new Node(document, services)); } if (space != null) { model.put("space", new Node(space, services)); } return model; } }