1. Project Clover database Tue Dec 20 2016 21:24:09 CET
  2. Package org.xwiki.rendering.macro.script

File AbstractJSR223ScriptMacro.java

 

Coverage histogram

../../../../../img/srcFileCovDistChart9.png
38% of files have more coverage

Code metrics

20
68
15
1
360
182
29
0.43
4.53
15
1.93

Classes

Class Line # Actions
AbstractJSR223ScriptMacro 60 68 0% 29 13
0.873786487.4%
 

Contributing tests

This file is covered by 40 tests. .

Source view

1    /*
2    * See the NOTICE file distributed with this work for additional
3    * information regarding copyright ownership.
4    *
5    * This is free software; you can redistribute it and/or modify it
6    * under the terms of the GNU Lesser General Public License as
7    * published by the Free Software Foundation; either version 2.1 of
8    * the License, or (at your option) any later version.
9    *
10    * This software is distributed in the hope that it will be useful,
11    * but WITHOUT ANY WARRANTY; without even the implied warranty of
12    * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13    * Lesser General Public License for more details.
14    *
15    * You should have received a copy of the GNU Lesser General Public
16    * License along with this software; if not, write to the Free
17    * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
18    * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
19    */
20    package org.xwiki.rendering.macro.script;
21   
22    import java.io.Reader;
23    import java.io.StringWriter;
24    import java.io.Writer;
25    import java.util.Collections;
26    import java.util.HashMap;
27    import java.util.List;
28    import java.util.Map;
29   
30    import javax.inject.Inject;
31    import javax.script.Compilable;
32    import javax.script.CompiledScript;
33    import javax.script.ScriptContext;
34    import javax.script.ScriptEngine;
35    import javax.script.ScriptEngineManager;
36    import javax.script.ScriptException;
37   
38    import org.apache.commons.lang3.StringUtils;
39    import org.xwiki.component.phase.InitializationException;
40    import org.xwiki.context.ExecutionContext;
41    import org.xwiki.properties.ConverterManager;
42    import org.xwiki.rendering.block.Block;
43    import org.xwiki.rendering.block.Block.Axes;
44    import org.xwiki.rendering.block.MetaDataBlock;
45    import org.xwiki.rendering.block.XDOM;
46    import org.xwiki.rendering.block.match.MetadataBlockMatcher;
47    import org.xwiki.rendering.listener.MetaData;
48    import org.xwiki.rendering.macro.MacroExecutionException;
49    import org.xwiki.rendering.macro.descriptor.ContentDescriptor;
50    import org.xwiki.rendering.transformation.MacroTransformationContext;
51    import org.xwiki.script.ScriptContextManager;
52   
53    /**
54    * Base Class for script evaluation macros based on JSR223.
55    *
56    * @param <P> the type of macro parameters bean.
57    * @version $Id: 41def580d9471e6c18342884ea666e3e9e47262c $
58    * @since 1.7M3
59    */
 
60    public abstract class AbstractJSR223ScriptMacro<P extends JSR223ScriptMacroParameters> extends AbstractScriptMacro<P>
61    implements PrivilegedScriptMacro
62    {
63   
64    /**
65    * The name of the binding containing the {@link ScriptContext} itself.
66    */
67    public static final String BINDING_CONTEXT = "context";
68   
69    /**
70    * The name of the "out" binding..
71    */
72    public static final String BINDING_OUT = "out";
73   
74    /**
75    * Key under which the Script Engines are saved in the Execution Context, see {@link #execution}.
76    */
77    private static final String EXECUTION_CONTEXT_ENGINE_KEY = "scriptEngines";
78   
79    /**
80    * The JSR223 Script Engine Manager we use to evaluate JSR223 scripts.
81    */
82    protected ScriptEngineManager scriptEngineManager;
83   
84    /**
85    * Used to get the current script context to give to script engine evaluation method.
86    */
87    @Inject
88    private ScriptContextManager scriptContextManager;
89   
90    @Inject
91    private ConverterManager converterManager;
92   
93    /**
94    * @param macroName the name of the macro (eg "groovy")
95    */
 
96  0 toggle public AbstractJSR223ScriptMacro(String macroName)
97    {
98  0 super(macroName, null, JSR223ScriptMacroParameters.class);
99    }
100   
101    /**
102    * @param macroName the name of the macro (eg "groovy")
103    * @param macroDescription the text description of the macro.
104    */
 
105  0 toggle public AbstractJSR223ScriptMacro(String macroName, String macroDescription)
106    {
107  0 super(macroName, macroDescription, JSR223ScriptMacroParameters.class);
108    }
109   
110    /**
111    * @param macroName the name of the macro (eg "groovy")
112    * @param macroDescription the text description of the macro.
113    * @param contentDescriptor the description of the macro content.
114    */
 
115  33 toggle public AbstractJSR223ScriptMacro(String macroName, String macroDescription, ContentDescriptor contentDescriptor)
116    {
117  33 super(macroName, macroDescription, contentDescriptor, JSR223ScriptMacroParameters.class);
118    }
119   
120    /**
121    * @param macroName the name of the macro (eg "groovy")
122    * @param macroDescription the text description of the macro.
123    * @param parametersBeanClass class of the parameters bean for this macro.
124    */
 
125  0 toggle public AbstractJSR223ScriptMacro(String macroName, String macroDescription,
126    Class< ? extends JSR223ScriptMacroParameters> parametersBeanClass)
127    {
128  0 super(macroName, macroDescription, parametersBeanClass);
129    }
130   
131    /**
132    * @param macroName the name of the macro (eg "groovy")
133    * @param macroDescription the text description of the macro.
134    * @param contentDescriptor the description of the macro content.
135    * @param parametersBeanClass class of the parameters bean for this macro.
136    */
 
137  12 toggle public AbstractJSR223ScriptMacro(String macroName, String macroDescription, ContentDescriptor contentDescriptor,
138    Class< ? extends JSR223ScriptMacroParameters> parametersBeanClass)
139    {
140  12 super(macroName, macroDescription, contentDescriptor, parametersBeanClass);
141    }
142   
 
143  41 toggle @Override
144    public void initialize() throws InitializationException
145    {
146  41 super.initialize();
147  41 this.scriptEngineManager = new ScriptEngineManager();
148    }
149   
 
150  13 toggle @Override
151    public boolean supportsInlineMode()
152    {
153  13 return true;
154    }
155   
156    /**
157    * Method to overwrite to indicate the script engine name.
158    *
159    * @param parameters the macro parameters.
160    * @param context the context of the macro transformation.
161    * @return the name of the script engine to use.
162    */
 
163  100 toggle protected String getScriptEngineName(P parameters, MacroTransformationContext context)
164    {
165  100 return context.getCurrentMacroBlock().getId().toLowerCase();
166    }
167   
168    /**
169    * Get the current ScriptContext and refresh it.
170    *
171    * @return the script context.
172    */
 
173  102 toggle protected ScriptContext getScriptContext()
174    {
175  102 return this.scriptContextManager.getScriptContext();
176    }
177   
 
178  113 toggle @Override
179    protected List<Block> evaluateBlock(P parameters, String content, MacroTransformationContext context)
180    throws MacroExecutionException
181    {
182  113 if (StringUtils.isEmpty(content)) {
183  0 return Collections.emptyList();
184    }
185   
186  113 String engineName = getScriptEngineName(parameters, context);
187   
188  113 List<Block> result;
189  113 if (engineName != null) {
190  103 try {
191  103 ScriptEngine engine = getScriptEngine(engineName);
192   
193  103 if (engine != null) {
194  102 result = evaluateBlock(engine, parameters, content, context);
195    } else {
196  1 throw new MacroExecutionException("Can't find script engine with name [" + engineName + "]");
197    }
198    } catch (ScriptException e) {
199  0 throw new MacroExecutionException("Failed to evaluate Script Macro for content [" + content + "]", e);
200    }
201   
202    } else {
203    // If no language identifier is provided, don't evaluate content
204  10 result = parseScriptResult(content, parameters, context);
205    }
206   
207  112 return result;
208    }
209   
210    /**
211    * Execute provided script and return {@link Block} based result.
212    *
213    * @param engine the script engine to use to evaluate the script.
214    * @param parameters the macro parameters.
215    * @param content the script to execute.
216    * @param context the context of the macro transformation.
217    * @return the result of script execution.
218    * @throws ScriptException failed to evaluate script
219    * @throws MacroExecutionException failed to evaluate provided content.
220    */
 
221  102 toggle protected List<Block> evaluateBlock(ScriptEngine engine, P parameters, String content,
222    MacroTransformationContext context) throws ScriptException, MacroExecutionException
223    {
224  102 List<Block> result;
225   
226  102 ScriptContext scriptContext = getScriptContext();
227   
228  102 Writer currentWriter = scriptContext.getWriter();
229  102 Reader currentReader = scriptContext.getReader();
230  102 Object currentContextBinding = scriptContext.getAttribute(BINDING_CONTEXT, ScriptContext.ENGINE_SCOPE);
231  102 Object currentFilename = scriptContext.getAttribute(ScriptEngine.FILENAME, ScriptContext.ENGINE_SCOPE);
232    // Some engines like Groovy are duplicating the writer in "out" binding
233  102 Object currentOut = scriptContext.getAttribute(BINDING_OUT, ScriptContext.ENGINE_SCOPE);
234    // Set standard javax.script.filename property
235  102 MetaDataBlock metaDataBlock =
236    context.getCurrentMacroBlock().getFirstBlock(new MetadataBlockMatcher(MetaData.SOURCE),
237    Axes.ANCESTOR_OR_SELF);
238  102 if (metaDataBlock != null) {
239  68 scriptContext.setAttribute(ScriptEngine.FILENAME, metaDataBlock.getMetaData().getMetaData(MetaData.SOURCE),
240    ScriptContext.ENGINE_SCOPE);
241    }
242   
243  102 try {
244  102 StringWriter stringWriter = new StringWriter();
245   
246    // set writer in script context
247  102 scriptContext.setWriter(stringWriter);
248   
249  102 Object scriptResult = eval(content, engine, scriptContext);
250   
251  102 result = convertScriptExecution(scriptResult, stringWriter, parameters, context);
252    } finally {
253    // restore current writer
254  102 scriptContext.setWriter(currentWriter);
255    // restore current reader
256  102 scriptContext.setReader(currentReader);
257    // restore "context" binding
258  102 scriptContext.setAttribute(BINDING_CONTEXT, currentContextBinding, ScriptContext.ENGINE_SCOPE);
259    // restore "javax.script.filename" binding
260  102 scriptContext.setAttribute(ScriptEngine.FILENAME, currentFilename, ScriptContext.ENGINE_SCOPE);
261    // restore "out" binding
262  102 scriptContext.setAttribute(BINDING_OUT, currentOut, ScriptContext.ENGINE_SCOPE);
263    }
264   
265  102 return result;
266    }
267   
 
268  102 toggle private List<Block> convertScriptExecution(Object scriptResult, StringWriter scriptContextWriter, P parameters,
269    MacroTransformationContext context) throws MacroExecutionException
270    {
271  102 List<Block> result;
272   
273  102 if (scriptResult instanceof XDOM) {
274  0 result = ((XDOM) scriptResult).getChildren();
275  102 } else if (scriptResult instanceof Block) {
276  1 result = Collections.singletonList((Block) scriptResult);
277  101 } else if (scriptResult instanceof List && !((List< ? >) scriptResult).isEmpty()
278    && ((List< ? >) scriptResult).get(0) instanceof Block) {
279  1 result = (List<Block>) scriptResult;
280    } else {
281    // If the Script Context writer is empty and the Script Result isn't, then convert the String Result
282    // to String and display it!
283  100 String contentToParse = scriptContextWriter.toString();
284  100 if (StringUtils.isEmpty(contentToParse) && scriptResult != null) {
285    // Convert the returned value into a String.
286  15 contentToParse = this.converterManager.convert(String.class, scriptResult);
287    }
288    // Run the wiki syntax parser on the Script returned content
289  100 result = parseScriptResult(contentToParse, parameters, context);
290    }
291   
292  102 return result;
293    }
294   
295    /**
296    * @param engineName the script engine name (eg "groovy", etc)
297    * @return the Script engine to use to evaluate the script
298    */
 
299  103 toggle private ScriptEngine getScriptEngine(String engineName)
300    {
301    // Look for a script engine in the Execution Context since we want the same engine to be used
302    // for all evals during the same execution lifetime.
303    // We must use the same engine because that engine may create an internal ClassLoader in which
304    // it loads new classes defined in the script and if we create a new engine then defined classes
305    // will be lost.
306    // However we also need to be able to execute several script Macros during a single execution request
307    // and for example the second macro could have jar parameters. In order to support this use case
308    // we ensure in AbstractScriptMacro to reuse the same thread context ClassLoader during the whole
309    // request execution.
310  103 ExecutionContext executionContext = this.execution.getContext();
311  103 Map<String, ScriptEngine> scriptEngines =
312    (Map<String, ScriptEngine>) executionContext.getProperty(EXECUTION_CONTEXT_ENGINE_KEY);
313  103 if (scriptEngines == null) {
314  95 scriptEngines = new HashMap<String, ScriptEngine>();
315  95 executionContext.setProperty(EXECUTION_CONTEXT_ENGINE_KEY, scriptEngines);
316    }
317  103 ScriptEngine engine = scriptEngines.get(engineName);
318   
319  103 if (engine == null) {
320  95 engine = this.scriptEngineManager.getEngineByName(engineName);
321  95 scriptEngines.put(engineName, engine);
322    }
323   
324  103 return engine;
325    }
326   
327    /**
328    * Execute the script.
329    *
330    * @param content the script to be executed by the script engine
331    * @param engine the script engine
332    * @param scriptContext the script context
333    * @return The value returned from the execution of the script.
334    * @throws ScriptException if an error occurrs in script. ScriptEngines should create and throw
335    * <code>ScriptException</code> wrappers for checked Exceptions thrown by underlying scripting
336    * implementations.
337    */
 
338  102 toggle protected Object eval(String content, ScriptEngine engine, ScriptContext scriptContext) throws ScriptException
339    {
340  102 return engine.eval(content, scriptContext);
341    }
342   
343    // /////////////////////////////////////////////////////////////////////
344    // Compiled scripts management
345   
346    /**
347    * Return a compiled version of the provided script.
348    *
349    * @param content the script to compile.
350    * @param engine the script engine.
351    * @return the compiled version of the script.
352    * @throws ScriptException failed to compile the script.
353    */
 
354  0 toggle protected CompiledScript getCompiledScript(String content, Compilable engine) throws ScriptException
355    {
356    // TODO: add caching
357   
358  0 return engine.compile(content);
359    }
360    }