model)
    {
        try
        {
            // compile the script based on the node content
            Script script;
            Context cx = Context.enter();
            try
            {
                script = cx.compileString(resolveScriptImports(source), "AlfrescoJS", 1, null);
            }
            finally
            {
                Context.exit();
            }
            return executeScriptImpl(script, model, true, "string script");
        }
        catch (Throwable err)
        {
            throw new ScriptException("Failed to execute supplied script: " + err.getMessage(), err);
        }
    }
    /**
     * Resolve the imports in the specified script. Supported include directives are of the following form:
     * 
     * <import resource="classpath:alfresco/includeme.js">
     * <import resource="workspace://SpacesStore/6f73de1b-d3b4-11db-80cb-112e6c2ea048">
     * <import resource="/Company Home/Data Dictionary/Scripts/includeme.js">
     * 
     * 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)
    {
        return ScriptResourceHelper.resolveScriptImports(script, this, logger);
    }
    
    /**
     * 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
     */
    public String loadScriptResource(String resource)
    {
        String result = null;
        
        if (resource.startsWith(PATH_CLASSPATH))
        {
            try
            {
                // Load from classpath
                String scriptClasspath = resource.substring(PATH_CLASSPATH.length());
                URL scriptResource = getClass().getClassLoader().getResource(scriptClasspath);
                if (scriptResource == null && scriptClasspath.startsWith("/"))
                {
                    // The Eclipse classloader prefers alfresco/foo to /alfresco/foo, try that
                    scriptResource = getClass().getClassLoader().getResource(scriptClasspath.substring(1));
                }
                if (scriptResource == null)
                {
                    throw new AlfrescoRuntimeException("Unable to locate included script classpath resource: " + resource);
                }
                InputStream stream = scriptResource.openStream();
                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, "UTF-8");
            }
            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.
     * @param secure        True if the script is considered secure and may access java.* libs directly
     * @param debugScriptName To identify the script in debug messages.
     * 
     * @return result of the script execution, can be null.
     * 
     * @throws AlfrescoRuntimeException
     */
    private Object executeScriptImpl(Script script, Map model, boolean secure, String debugScriptName)
        throws AlfrescoRuntimeException
    {
        long startTime = 0;
        if (callLogger.isDebugEnabled())
        {
            callLogger.debug(debugScriptName+" Start");
            startTime = System.nanoTime();
        }
        
        // Convert the model
        model = convertToRhinoModel(model);
        
        Context cx = Context.enter();
        try
        {
            // Create a thread-specific scope from one of the shared scopes.
            // See http://www.mozilla.org/rhino/scopes.html
            cx.setWrapFactory(wrapFactory);
            Scriptable scope;
            if (this.shareSealedScopes)
            {
                Scriptable sharedScope = secure ? this.nonSecureScope : this.secureScope;
                scope = cx.newObject(sharedScope);
                scope.setPrototype(sharedScope);
                scope.setParentScope(null);
            }
            else
            {
                scope = initScope(cx, secure, false);
            }
            
            // there's always a model, if only to hold the util objects
            if (model == null)
            {
                model = new HashMap();
            }
            
            // add the global scripts
            for (ProcessorExtension ex : this.processorExtensions.values()) 
            {
                model.put(ex.getExtensionName(), ex);
            }
            
            // insert supplied object model into root of the default scope
            for (String key : model.keySet())
            {
            	try
            	{
	                // 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);
            	}
            	catch(AuthenticationException e)
            	{
            		// ok, log and don't add to the root scope
            		logger.info("Unable to add " + key + " to root scope: ", e);
            	}
            }
            
            // execute the script and return the result
            Object result = script.exec(cx, scope);
            
            // extract java object result if wrapped by Rhino 
            return valueConverter.convertValueForJava(result);
        }
        catch (WrappedException w)
        {
            if (callLogger.isDebugEnabled())
            {
                callLogger.debug(debugScriptName+" Exception", w);
            }
            Throwable err = w.getWrappedException();
            if (err instanceof RuntimeException)
            {
                throw (RuntimeException)err;
            }
            throw new AlfrescoRuntimeException(err.getMessage(), err);
        }
        catch (Throwable err)
        {
            if (callLogger.isDebugEnabled())
            {
                callLogger.debug(debugScriptName+" Exception", err);
            }
            throw new AlfrescoRuntimeException(err.getMessage(), err);
        }
        finally
        {
            Context.exit();
            
            if (callLogger.isDebugEnabled())
            {
                long endTime = System.nanoTime();
                callLogger.debug(debugScriptName+" End " + (endTime - startTime)/1000000 + " ms");
            }
        }
    }
    
    /**
     * Converts the passed model into a Rhino model
     * 
     * @param model     the model
     * 
     * @return Map the converted model
     */
    private Map convertToRhinoModel(Map model)
    {
    	Map newModel = null;
    	if (model != null)
    	{
	        newModel = new HashMap(model.size());
	        for (Map.Entry entry : model.entrySet())
	        {
	            if (entry.getValue() instanceof NodeRef)
	            {
	                newModel.put(entry.getKey(), new ScriptNode((NodeRef)entry.getValue(), this.services));
	            }
	            else
	            {
	                newModel.put(entry.getKey(), entry.getValue());
	            }
	        }
    	}
    	else
    	{
    		newModel = new HashMap(1, 1.0f);
    	}
        return newModel;
    }
    
    /**
     * Rhino script value wraper
     */
    private static class RhinoWrapFactory extends WrapFactory
    {
    	/* (non-Javadoc)
    	 * @see org.mozilla.javascript.WrapFactory#wrapAsJavaObject(org.mozilla.javascript.Context, org.mozilla.javascript.Scriptable, java.lang.Object, java.lang.Class)
    	 */
        public Scriptable wrapAsJavaObject(Context cx, Scriptable scope, Object javaObject, Class staticType)
        {
            if (javaObject instanceof Map && !(javaObject instanceof ScriptableHashMap))
            {
                return new NativeMap(scope, (Map)javaObject);
            }
            return super.wrapAsJavaObject(cx, scope, javaObject, staticType);
        }
    }
    
    /**
     * Pre initializes two scope objects (one secure and one not) with the standard objects preinitialised.
     * This saves on very expensive calls to reinitialize a new scope on every web script execution. See
     * http://www.mozilla.org/rhino/scopes.html
     * 
     * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
     */
    public void afterPropertiesSet() throws Exception
    {
        // Initialize the secure scope
        Context cx = Context.enter();
        try
        {
            cx.setWrapFactory(wrapFactory);
            this.secureScope = initScope(cx, false, true);
        }
        finally
        {
            Context.exit();
        }
        
        // Initialize the non-secure scope
        cx = Context.enter();
        try
        {
            cx.setWrapFactory(wrapFactory);
            this.nonSecureScope = initScope(cx, true, true);
        }
        finally
        {
            Context.exit();
        }
    }
    
    /**
     * Initializes a scope for script execution. 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.
     * 
     * @param cx        the thread execution context
     * @param secure    Do we consider the script secure? When false this ensures the script may not
     *                  access insecure java.* libraries or import any other classes for direct access - only the
     *                  configured root host objects will be available to the script writer.
     * @param sealed    Should the scope be sealed, making it immutable? This should be true if a scope
     *                  is to be reused.
     * @return the scope object
     */
    protected Scriptable initScope(Context cx, boolean secure, boolean sealed)
    {
        Scriptable scope;
        if (secure)
        {
            // Initialise the non-secure scope
            // allow access to all libraries and objects, including the importer
            // @see http://www.mozilla.org/rhino/ScriptingJava.html
            scope = new ImporterTopLevel(cx, sealed);
        }
        else
        {
            // Initialise the secure scope
            scope = cx.initStandardObjects(null, sealed);
            // remove security issue related objects - this ensures the script may not access
            // unsecure java.* libraries or import any other classes for direct access - only
            // the configured root host objects will be available to the script writer
            scope.delete("Packages");
            scope.delete("getClass");
            scope.delete("java");
        }
        return scope;
    }
}