View Javadoc

1   /**
2    * Copyright (C) 2005-2009 Alfresco Software Limited.
3    *
4    * This file is part of the Spring Surf Extension project.
5    *
6    * Licensed under the Apache License, Version 2.0 (the "License");
7    * you may not use this file except in compliance with the License.
8    * You may obtain a copy of the License at
9    *
10   *  http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing, software
13   * distributed under the License is distributed on an "AS IS" BASIS,
14   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15   * See the License for the specific language governing permissions and
16   * limitations under the License.
17   */
18  
19  package org.springframework.extensions.webscripts.processor;
20  
21  import java.io.ByteArrayOutputStream;
22  import java.io.IOException;
23  import java.io.InputStream;
24  import java.util.HashMap;
25  import java.util.Map;
26  import java.util.concurrent.ConcurrentHashMap;
27  
28  import org.apache.commons.logging.Log;
29  import org.apache.commons.logging.LogFactory;
30  import org.mozilla.javascript.Context;
31  import org.mozilla.javascript.ImporterTopLevel;
32  import org.mozilla.javascript.Script;
33  import org.mozilla.javascript.Scriptable;
34  import org.mozilla.javascript.ScriptableObject;
35  import org.mozilla.javascript.WrapFactory;
36  import org.mozilla.javascript.WrappedException;
37  import org.springframework.extensions.surf.core.scripts.ScriptResourceHelper;
38  import org.springframework.extensions.surf.core.scripts.ScriptResourceLoader;
39  import org.springframework.extensions.surf.exception.WebScriptsPlatformException;
40  import org.springframework.extensions.webscripts.NativeMap;
41  import org.springframework.extensions.webscripts.ScriptContent;
42  import org.springframework.extensions.webscripts.ScriptValueConverter;
43  import org.springframework.extensions.webscripts.WebScriptException;
44  import org.springframework.util.FileCopyUtils;
45  
46  
47  /**
48   * JS Script Processor for Alfresco Web Framework
49   * 
50   * @author davidc
51   * @author kevinr
52   */
53  public class JSScriptProcessor extends AbstractScriptProcessor implements ScriptResourceLoader
54  {
55      private static final Log logger = LogFactory.getLog(JSScriptProcessor.class);
56      private static WrapFactory wrapFactory = new PresentationWrapFactory(); 
57  
58      private static final String PATH_CLASSPATH = "classpath:";
59          
60      /** Pre initialized secure scope object. */
61      private Scriptable secureScope;
62      
63      /** Pre initialized non secure scope object. */
64      private Scriptable nonSecureScope;
65      
66      /** Flag to enable or disable runtime script compliation */
67      private boolean compile = true;
68      
69      /** Cache of runtime compiled script instances */
70      private Map<String, Script> scriptCache = new ConcurrentHashMap<String, Script>(256);
71  
72      /**
73       * @param compile   the compile flag to set
74       */
75      public void setCompile(boolean compile)
76      {
77          this.compile = compile;
78      }
79      
80      /* (non-Javadoc)
81       * @see org.alfresco.processor.Processor#getExtension()
82       */
83      public String getExtension()
84      {
85          return "js";
86      }
87  
88      /* (non-Javadoc)
89       * @see org.alfresco.processor.Processor#getName()
90       */
91      public String getName()
92      {
93          return "javascript";
94      }
95      
96      /* (non-Javadoc)
97       * @see org.alfresco.web.scripts.processor.AbstractScriptProcessor#init()
98       */
99      public void init()
100     {
101         super.init();
102         
103         this.initProcessor();        
104     }
105 
106     /* (non-Javadoc)
107      * @see org.alfresco.web.scripts.ScriptProcessor#findScript(java.lang.String)
108      */
109     public ScriptContent findScript(String path)
110     {
111         return getScriptLoader().getScript(path);
112     }
113 
114     /* (non-Javadoc)
115      * @see org.alfresco.web.scripts.ScriptProcessor#executeScript(java.lang.String, java.util.Map)
116      */
117     public Object executeScript(String path, Map<String, Object> model)
118     {
119         // locate script within web script stores
120         ScriptContent scriptLocation = findScript(path);
121         if (scriptLocation == null)
122         {
123             throw new WebScriptException("Unable to locate script " + path);
124         }
125         // execute script
126         return executeScript(scriptLocation, model);
127     }
128 
129     /* (non-Javadoc)
130      * @see org.alfresco.web.scripts.ScriptProcessor#executeScript(org.alfresco.web.scripts.ScriptContent, java.util.Map)
131      */
132     public Object executeScript(ScriptContent location, Map<String, Object> model)
133     {
134         try
135         {
136             // test the cache for a pre-compiled script matching our path
137             String path = location.getPath();
138             Script script = null;
139             if (this.compile && location.isCachable())
140             {
141                 script = this.scriptCache.get(path);
142             }
143             if (script == null)
144             {
145                 if (logger.isDebugEnabled())
146                     logger.debug("Resolving and compiling script path: " + path);
147                 
148                 // retrieve script content and resolve imports
149                 ByteArrayOutputStream os = new ByteArrayOutputStream();
150                 FileCopyUtils.copy(location.getInputStream(), os);  // both streams are closed
151                 byte[] bytes = os.toByteArray();
152                 String source = new String(bytes, "UTF-8");
153                 source = ScriptResourceHelper.resolveScriptImports(source, this, logger);
154                 
155                 // compile the script and cache the result
156                 Context cx = Context.enter();
157                 try
158                 {
159                     script = cx.compileString(source, path, 1, null);
160                     
161                     // We do not worry about more than one user thread compiling the same script.
162                     // If more than one request thread compiles the same script and adds it to the
163                     // cache that does not matter - the results will be the same. Therefore we
164                     // rely on the ConcurrentHashMap impl to deal both with ensuring the safety of the
165                     // underlying structure with asynchronous get/put operations and for fast
166                     // multi-threaded access to the common cache.
167                     if (this.compile && location.isCachable())
168                     {
169                         this.scriptCache.put(path, script);
170                     }
171                 }
172                 finally
173                 {
174                     Context.exit();
175                 }
176             }
177             
178             return executeScriptImpl(script, model, location.isSecure());
179         }
180         catch (Throwable e)
181         {
182             throw new WebScriptException("Failed to load script '" + location.toString() + "': " + e.getMessage(), e);
183         }
184     }
185 
186     /**
187      * Load a script content from the specific resource path.
188      *  
189      * @param resource      Script resource to load. Supports either classpath: prefix syntax or a
190      *                      resource path within the webscript stores. 
191      * 
192      * @return the content from the resource, null if not recognised format
193      * 
194      * @throws ConfigServiceRuntimeException on any IO or ContentIO error
195      */
196     public String loadScriptResource(String resource)
197     {
198         if (resource.startsWith(PATH_CLASSPATH))
199         {
200             try
201             {
202                 // load from classpath
203                 String scriptClasspath = resource.substring(PATH_CLASSPATH.length());
204                 InputStream stream = getClass().getClassLoader().getResource(scriptClasspath).openStream();
205                 if (stream == null)
206                 {
207                     throw new WebScriptsPlatformException("Unable to load included script classpath resource: " + resource);
208                 }
209                 ByteArrayOutputStream os = new ByteArrayOutputStream();
210                 FileCopyUtils.copy(stream, os);  // both streams are closed
211                 byte[] bytes = os.toByteArray();
212                 // create the string from the byte[] using encoding if necessary
213                 return new String(bytes, "UTF-8");
214             }
215             catch (IOException err)
216             {
217                 throw new WebScriptsPlatformException("Unable to load included script classpath resource: " + resource);
218             }
219         }
220         else
221         {
222             // locate script within web script stores
223             ScriptContent scriptLocation = findScript(resource);
224             if (scriptLocation == null)
225             {
226                 throw new WebScriptException("Unable to locate script " + resource);
227             }
228             try
229             {   
230                 ByteArrayOutputStream os = new ByteArrayOutputStream();
231                 FileCopyUtils.copy(scriptLocation.getInputStream(), os);  // both streams are closed
232                 byte[] bytes = os.toByteArray();
233                 return new String(bytes, "UTF-8");
234             }
235             catch (Throwable e)
236             {
237                 throw new WebScriptException(
238                         "Failed to load script '" + scriptLocation.toString() + "': " + e.getMessage(), e);
239             }
240         }
241     }
242 
243     /**
244      * Execute the supplied script content.
245      * 
246      * @param script        The script to execute.
247      * @param model         Data model containing objects to be added to the root scope.
248      * @param secure        True if the script is considered secure and may access java.* libs directly
249      * 
250      * @return result of the script execution, can be null.
251      * 
252      * @throws ConfigServiceRuntimeException
253      */
254     private Object executeScriptImpl(Script script, Map<String, Object> model, boolean secure)
255     {
256         // execute script
257         long startTime = 0;
258         if (logger.isDebugEnabled())
259         {
260             startTime = System.nanoTime();
261         }
262         
263         Context cx = Context.enter();
264         cx.setOptimizationLevel(1);
265         try
266         {
267             // Create a thread-specific scope from one of the shared scopes.
268             // See http://www.mozilla.org/rhino/scopes.html
269             cx.setWrapFactory(wrapFactory);
270             Scriptable sharedScope = secure ? this.nonSecureScope : this.secureScope;
271             Scriptable scope = cx.newObject(sharedScope);
272             scope.setPrototype(sharedScope);
273             scope.setParentScope(null);
274             
275             // there's always a model, if only to hold the extension objects
276             if (model == null)
277             {
278                 model = new HashMap<String, Object>();
279             }
280             
281             // add the global scripts
282             addProcessorModelExtensions(model);
283             
284             // insert supplied object model into root of the default scope
285             for (String key : model.keySet())
286             {
287                 Object obj = model.get(key);
288                 ScriptableObject.putProperty(scope, key, obj);
289             }
290             
291             // execute the script and return the result
292             Object result = script.exec(cx, scope);
293             return result;
294         }
295         catch (WrappedException w)
296         {
297             Throwable err = w.getWrappedException();
298             throw new WebScriptException(err.getMessage(), err);
299         }
300         catch (Throwable e)
301         {
302             throw new WebScriptException(e.getMessage(), e);
303         }
304         finally
305         {
306             Context.exit();
307 
308             if (logger.isDebugEnabled())
309             {
310                 long endTime = System.nanoTime();
311                 logger.debug("Time to execute script: " + (endTime - startTime)/1000000f + "ms");
312             }
313         }
314     }
315 
316     /* (non-Javadoc)
317      * @see org.alfresco.web.scripts.ScriptProcessor#unwrapValue(java.lang.Object)
318      */
319     public Object unwrapValue(Object value)
320     {
321         return ScriptValueConverter.unwrapValue(value);
322     }
323 
324     /* (non-Javadoc)
325      * @see org.alfresco.web.scripts.ScriptProcessor#reset()
326      */
327     public void reset()
328     {
329         init();
330         this.scriptCache.clear();
331     }
332     
333     /**
334      * Inits the processor.
335      */
336     protected void initProcessor()
337     {
338         // Initialise the secure scope
339         Context cx = Context.enter();
340         try
341         {
342             cx.setWrapFactory(wrapFactory);
343             this.secureScope = cx.initStandardObjects();
344             
345             // remove security issue related objects - this ensures the script may not access
346             // unsecure java.* libraries or import any other classes for direct access - only
347             // the configured root host objects will be available to the script writer
348             this.secureScope.delete("Packages");
349             this.secureScope.delete("getClass");
350             this.secureScope.delete("java");
351         }
352         finally
353         {
354             Context.exit();
355         }
356         
357         // Initialise the non-secure scope
358         cx = Context.enter();
359         try
360         {
361             cx.setWrapFactory(wrapFactory);
362             
363             // allow access to all libraries and objects, including the importer
364             // @see http://www.mozilla.org/rhino/ScriptingJava.html
365             this.nonSecureScope = new ImporterTopLevel(cx);
366         }
367         finally
368         {
369             Context.exit();
370         }
371     }
372 
373 
374     /**
375      * Wrap Factory for Rhino Script Engine
376      * 
377      * @author davidc
378      */
379     public static class PresentationWrapFactory extends WrapFactory
380     {
381         /* (non-Javadoc)
382          * @see org.mozilla.javascript.WrapFactory#wrapAsJavaObject(org.mozilla.javascript.Context, org.mozilla.javascript.Scriptable, java.lang.Object, java.lang.Class)
383          */
384         public Scriptable wrapAsJavaObject(Context cx, Scriptable scope, Object javaObject, Class staticType)
385         {
386             if (javaObject instanceof Map)
387             {
388                 return new NativeMap(scope, (Map)javaObject);
389             }
390             return super.wrapAsJavaObject(cx, scope, javaObject, staticType);
391         }
392     }    
393 }