/* * See the NOTICE file distributed with this work for additional * information regarding copyright ownership. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package org.xwiki.rendering.macro.script; import java.io.Reader; import java.io.StringWriter; import java.io.Writer; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.inject.Inject; import javax.script.Compilable; import javax.script.CompiledScript; import javax.script.ScriptContext; import javax.script.ScriptEngine; import javax.script.ScriptEngineManager; import javax.script.ScriptException; import org.apache.commons.lang3.StringUtils; import org.xwiki.component.phase.InitializationException; import org.xwiki.context.ExecutionContext; import org.xwiki.properties.ConverterManager; import org.xwiki.rendering.block.Block; import org.xwiki.rendering.block.Block.Axes; import org.xwiki.rendering.block.MetaDataBlock; import org.xwiki.rendering.block.XDOM; import org.xwiki.rendering.block.match.MetadataBlockMatcher; import org.xwiki.rendering.listener.MetaData; import org.xwiki.rendering.macro.MacroExecutionException; import org.xwiki.rendering.macro.descriptor.ContentDescriptor; import org.xwiki.rendering.transformation.MacroTransformationContext; import org.xwiki.script.ScriptContextManager; /** * Base Class for script evaluation macros based on JSR223. * * @param <P> the type of macro parameters bean. * @version $Id: f3fe40364d6a921bcd650638f69a256a8f9f21ed $ * @since 1.7M3 */ public abstract class AbstractJSR223ScriptMacro<P extends JSR223ScriptMacroParameters> extends AbstractScriptMacro<P> implements PrivilegedScriptMacro { /** * The name of the binding containing the {@link ScriptContext} itself. */ public static final String BINDING_CONTEXT = "context"; /** * The name of the "out" binding.. */ public static final String BINDING_OUT = "out"; /** * Key under which the Script Engines are saved in the Execution Context, see {@link #execution}. */ private static final String EXECUTION_CONTEXT_ENGINE_KEY = "scriptEngines"; /** * The JSR223 Script Engine Manager we use to evaluate JSR223 scripts. */ protected ScriptEngineManager scriptEngineManager; /** * Used to get the current script context to give to script engine evaluation method. */ @Inject private ScriptContextManager scriptContextManager; @Inject private ConverterManager converterManager; /** * @param macroName the name of the macro (eg "groovy") */ public AbstractJSR223ScriptMacro(String macroName) { super(macroName, null, JSR223ScriptMacroParameters.class); } /** * @param macroName the name of the macro (eg "groovy") * @param macroDescription the text description of the macro. */ public AbstractJSR223ScriptMacro(String macroName, String macroDescription) { super(macroName, macroDescription, JSR223ScriptMacroParameters.class); } /** * @param macroName the name of the macro (eg "groovy") * @param macroDescription the text description of the macro. * @param contentDescriptor the description of the macro content. */ public AbstractJSR223ScriptMacro(String macroName, String macroDescription, ContentDescriptor contentDescriptor) { super(macroName, macroDescription, contentDescriptor, JSR223ScriptMacroParameters.class); } /** * @param macroName the name of the macro (eg "groovy") * @param macroDescription the text description of the macro. * @param parametersBeanClass class of the parameters bean for this macro. */ public AbstractJSR223ScriptMacro(String macroName, String macroDescription, Class< ? extends JSR223ScriptMacroParameters> parametersBeanClass) { super(macroName, macroDescription, parametersBeanClass); } /** * @param macroName the name of the macro (eg "groovy") * @param macroDescription the text description of the macro. * @param contentDescriptor the description of the macro content. * @param parametersBeanClass class of the parameters bean for this macro. */ public AbstractJSR223ScriptMacro(String macroName, String macroDescription, ContentDescriptor contentDescriptor, Class< ? extends JSR223ScriptMacroParameters> parametersBeanClass) { super(macroName, macroDescription, contentDescriptor, parametersBeanClass); } @Override public void initialize() throws InitializationException { super.initialize(); this.scriptEngineManager = new ScriptEngineManager(); } @Override public boolean supportsInlineMode() { return true; } /** * Method to overwrite to indicate the script engine name. * * @param parameters the macro parameters. * @param context the context of the macro transformation. * @return the name of the script engine to use. */ protected String getScriptEngineName(P parameters, MacroTransformationContext context) { return context.getCurrentMacroBlock().getId().toLowerCase(); } /** * Get the current ScriptContext and refresh it. * * @return the script context. */ protected ScriptContext getScriptContext() { return this.scriptContextManager.getScriptContext(); } @Override protected List<Block> evaluateBlock(P parameters, String content, MacroTransformationContext context) throws MacroExecutionException { if (StringUtils.isEmpty(content)) { return Collections.emptyList(); } String engineName = getScriptEngineName(parameters, context); List<Block> result; if (engineName != null) { try { ScriptEngine engine = getScriptEngine(engineName); if (engine != null) { result = evaluateBlock(engine, parameters, content, context); } else { throw new MacroExecutionException("Can't find script engine with name [" + engineName + "]"); } } catch (ScriptException e) { throw new MacroExecutionException("Failed to evaluate Script Macro for content [" + content + "]", e); } } else { // If no language identifier is provided, don't evaluate content result = parseScriptResult(content, parameters, context); } return result; } /** * Execute provided script and return {@link Block} based result. * * @param engine the script engine to use to evaluate the script. * @param parameters the macro parameters. * @param content the script to execute. * @param context the context of the macro transformation. * @return the result of script execution. * @throws ScriptException failed to evaluate script * @throws MacroExecutionException failed to evaluate provided content. */ protected List<Block> evaluateBlock(ScriptEngine engine, P parameters, String content, MacroTransformationContext context) throws ScriptException, MacroExecutionException { List<Block> result; ScriptContext scriptContext = getScriptContext(); Writer currentWriter = scriptContext.getWriter(); Reader currentReader = scriptContext.getReader(); Object currentContextBinding = scriptContext.getAttribute(BINDING_CONTEXT, ScriptContext.ENGINE_SCOPE); Object currentFilename = scriptContext.getAttribute(ScriptEngine.FILENAME, ScriptContext.ENGINE_SCOPE); // Some engines like Groovy are duplicating the writer in "out" binding Object currentOut = scriptContext.getAttribute(BINDING_OUT, ScriptContext.ENGINE_SCOPE); // Set standard javax.script.filename property MetaDataBlock metaDataBlock = context.getCurrentMacroBlock().getFirstBlock(new MetadataBlockMatcher(MetaData.SOURCE), Axes.ANCESTOR_OR_SELF); if (metaDataBlock != null) { scriptContext.setAttribute(ScriptEngine.FILENAME, metaDataBlock.getMetaData().getMetaData(MetaData.SOURCE), ScriptContext.ENGINE_SCOPE); } try { StringWriter stringWriter = new StringWriter(); // set writer in script context scriptContext.setWriter(stringWriter); Object scriptResult = eval(content, engine, scriptContext); result = convertScriptExecution(scriptResult, stringWriter, parameters, context); } finally { // restore current writer scriptContext.setWriter(currentWriter); // restore current reader scriptContext.setReader(currentReader); // restore "context" binding scriptContext.setAttribute(BINDING_CONTEXT, currentContextBinding, ScriptContext.ENGINE_SCOPE); // restore "javax.script.filename" binding scriptContext.setAttribute(ScriptEngine.FILENAME, currentFilename, ScriptContext.ENGINE_SCOPE); // restore "out" binding scriptContext.setAttribute(BINDING_OUT, currentOut, ScriptContext.ENGINE_SCOPE); } return result; } private List<Block> convertScriptExecution(Object scriptResult, StringWriter scriptContextWriter, P parameters, MacroTransformationContext context) throws MacroExecutionException { List<Block> result; if (scriptResult instanceof XDOM) { result = ((XDOM) scriptResult).getChildren(); } else if (scriptResult instanceof Block) { result = Collections.singletonList((Block) scriptResult); } else if (scriptResult instanceof List && !((List< ? >) scriptResult).isEmpty() && ((List< ? >) scriptResult).get(0) instanceof Block) { result = (List<Block>) scriptResult; } else if (scriptResult instanceof Class) { // Class result means class definition and we don't want to print anything in this case result = Collections.emptyList(); } else { // If the Script Context writer is empty and the Script Result isn't, then convert the String Result // to String and display it! String contentToParse = scriptContextWriter.toString(); if (StringUtils.isEmpty(contentToParse) && scriptResult != null) { // Convert the returned value into a String. contentToParse = this.converterManager.convert(String.class, scriptResult); } // Run the wiki syntax parser on the Script returned content result = parseScriptResult(contentToParse, parameters, context); } return result; } /** * @param engineName the script engine name (eg "groovy", etc) * @return the Script engine to use to evaluate the script */ private ScriptEngine getScriptEngine(String engineName) { // Look for a script engine in the Execution Context since we want the same engine to be used // for all evals during the same execution lifetime. // We must use the same engine because that engine may create an internal ClassLoader in which // it loads new classes defined in the script and if we create a new engine then defined classes // will be lost. // However we also need to be able to execute several script Macros during a single execution request // and for example the second macro could have jar parameters. In order to support this use case // we ensure in AbstractScriptMacro to reuse the same thread context ClassLoader during the whole // request execution. ExecutionContext executionContext = this.execution.getContext(); Map<String, ScriptEngine> scriptEngines = (Map<String, ScriptEngine>) executionContext.getProperty(EXECUTION_CONTEXT_ENGINE_KEY); if (scriptEngines == null) { scriptEngines = new HashMap<String, ScriptEngine>(); executionContext.setProperty(EXECUTION_CONTEXT_ENGINE_KEY, scriptEngines); } ScriptEngine engine = scriptEngines.get(engineName); if (engine == null) { engine = this.scriptEngineManager.getEngineByName(engineName); scriptEngines.put(engineName, engine); } return engine; } /** * Execute the script. * * @param content the script to be executed by the script engine * @param engine the script engine * @param scriptContext the script context * @return The value returned from the execution of the script. * @throws ScriptException if an error occurrs in script. ScriptEngines should create and throw * <code>ScriptException</code> wrappers for checked Exceptions thrown by underlying scripting * implementations. */ protected Object eval(String content, ScriptEngine engine, ScriptContext scriptContext) throws ScriptException { return engine.eval(content, scriptContext); } // ///////////////////////////////////////////////////////////////////// // Compiled scripts management /** * Return a compiled version of the provided script. * * @param content the script to compile. * @param engine the script engine. * @return the compiled version of the script. * @throws ScriptException failed to compile the script. */ protected CompiledScript getCompiledScript(String content, Compilable engine) throws ScriptException { // TODO: add caching return engine.compile(content); } }