/*
* 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.StringReader;
import java.util.Collections;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Named;
import org.apache.commons.lang3.StringUtils;
import org.xwiki.context.Execution;
import org.xwiki.observation.ObservationManager;
import org.xwiki.rendering.block.Block;
import org.xwiki.rendering.block.MacroBlock;
import org.xwiki.rendering.macro.AbstractSignableMacro;
import org.xwiki.rendering.macro.MacroContentParser;
import org.xwiki.rendering.macro.MacroExecutionException;
import org.xwiki.rendering.macro.descriptor.ContentDescriptor;
import org.xwiki.rendering.parser.ParseException;
import org.xwiki.rendering.parser.Parser;
import org.xwiki.rendering.transformation.MacroTransformationContext;
import org.xwiki.rendering.util.ParserUtils;
import org.xwiki.script.event.ScriptEvaluatedEvent;
import org.xwiki.script.event.ScriptEvaluatingEvent;
/**
* Base Class for script evaluation macros.
* <p>
* It is not obvious to see how macro execution works just from looking at the code. A lot of checking and
* initialization is done in listeners to the {@link org.xwiki.script.event.ScriptEvaluatingEvent} and
* {@link org.xwiki.script.event.ScriptEvaluatedEvent}. E.g. the check for programming rights for JSR223 scripts, check
* for nested script macros and selecting the right class loader is done there.
* </p>
*
* @param <P> the type of macro parameters bean.
* @version $Id: 4f35ffec60e0860ada33ac82e04b3de7621bb2f8 $
* @since 1.7M3
*/
public abstract class AbstractScriptMacro<P extends ScriptMacroParameters> extends AbstractSignableMacro<P> implements
ScriptMacro
{
/**
* The default description of the script macro content.
*/
protected static final String CONTENT_DESCRIPTION = "the script to execute";
/**
* Used to find if the current document's author has programming rights.
*
* @deprecated since 2.5M1 (not used any more)
*/
@Inject
@Deprecated
protected org.xwiki.bridge.DocumentAccessBridge documentAccessBridge;
/**
* Used by subclasses.
*/
@Inject
protected Execution execution;
/**
* Used to parse the result of the script execution into a XDOM object when the macro is configured by the user to
* not interpret wiki syntax.
*/
@Inject
@Named("plain/1.0")
private Parser plainTextParser;
/**
* The parser used to parse box content and box title parameter.
*/
@Inject
private MacroContentParser contentParser;
/**
* Observation manager used to sent evaluation events.
*/
@Inject
private ObservationManager observation;
/**
* Utility to remove the top level paragraph.
*/
private ParserUtils parserUtils = new ParserUtils();
/**
* @param macroName the name of the macro (eg "groovy")
*/
public AbstractScriptMacro(String macroName)
{
super(macroName, null, ScriptMacroParameters.class);
setDefaultCategory(DEFAULT_CATEGORY_DEVELOPMENT);
}
/**
* @param macroName the name of the macro (eg "groovy")
* @param macroDescription the text description of the macro.
*/
public AbstractScriptMacro(String macroName, String macroDescription)
{
super(macroName, macroDescription, ScriptMacroParameters.class);
setDefaultCategory(DEFAULT_CATEGORY_DEVELOPMENT);
}
/**
* @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 AbstractScriptMacro(String macroName, String macroDescription, ContentDescriptor contentDescriptor)
{
super(macroName, macroDescription, contentDescriptor, ScriptMacroParameters.class);
setDefaultCategory(DEFAULT_CATEGORY_DEVELOPMENT);
}
/**
* @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 AbstractScriptMacro(String macroName, String macroDescription,
Class< ? extends ScriptMacroParameters> parametersBeanClass)
{
super(macroName, macroDescription, parametersBeanClass);
setDefaultCategory(DEFAULT_CATEGORY_DEVELOPMENT);
}
/**
* @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 AbstractScriptMacro(String macroName, String macroDescription, ContentDescriptor contentDescriptor,
Class< ? extends ScriptMacroParameters> parametersBeanClass)
{
super(macroName, macroDescription, contentDescriptor, parametersBeanClass);
setDefaultCategory(DEFAULT_CATEGORY_DEVELOPMENT);
}
@Override
public List<Block> execute(P parameters, String content, MacroTransformationContext context)
throws MacroExecutionException
{
List<Block> result = Collections.emptyList();
if (StringUtils.isNotEmpty(content)) {
try {
// send evaluation starts event
ScriptEvaluatingEvent event = new ScriptEvaluatingEvent(getDescriptor().getId().getId());
this.observation.notify(event, context, parameters);
if (event.isCanceled()) {
throw new MacroExecutionException(event.getReason());
}
// 2) Run script engine on macro block content
List<Block> blocks = evaluateBlock(parameters, content, context);
if (parameters.isOutput()) {
result = blocks;
}
} finally {
// send evaluation finished event
this.observation.notify(new ScriptEvaluatedEvent(getDescriptor().getId().getId()), context, parameters);
}
}
return result;
}
/**
* Convert script result as a {@link Block} list.
*
* @param content the script result to parse.
* @param parameters the macro parameters.
* @param context the context of the macro transformation.
* @return the {@link Block}s.
* @throws MacroExecutionException Failed to find source parser.
* @since 2.1M1
*/
protected List<Block> parseScriptResult(String content, P parameters, MacroTransformationContext context)
throws MacroExecutionException
{
List<Block> result;
if (parameters.isWiki()) {
result = parseSourceSyntax(content, context);
} else {
try {
result = this.plainTextParser.parse(new StringReader(content)).getChildren();
} catch (ParseException e) {
// This shouldn't happen since the parser cannot throw an exception since the source is a memory
// String.
throw new MacroExecutionException("Failed to parse link label as plain text", e);
}
}
// 3) If in inline mode remove any top level paragraph
if (context.isInline()) {
this.parserUtils.removeTopLevelParagraph(result);
// Make sure included macro is inline when script macro itself is inline
// TODO: use inline parser instead
if (!result.isEmpty() && result.get(0) instanceof MacroBlock && !((MacroBlock) result.get(0)).isInline()) {
MacroBlock macro = (MacroBlock) result.get(0);
result.set(0, new MacroBlock(macro.getId(), macro.getParameters(), macro.getContent(), true));
}
}
return result;
}
/**
* Execute provided 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 MacroExecutionException failed to evaluate provided content.
* @deprecated since 2.4M2 use {@link #evaluateString(ScriptMacroParameters, String, MacroTransformationContext)}
* instead
*/
@Deprecated
protected String evaluate(P parameters, String content, MacroTransformationContext context)
throws MacroExecutionException
{
return "";
}
/**
* Execute provided script and return {@link String} based result.
*
* @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 MacroExecutionException failed to evaluate provided content.
* @since 2.4M2
*/
protected String evaluateString(P parameters, String content, MacroTransformationContext context)
throws MacroExecutionException
{
// Call old method for retro-compatibility
return evaluate(parameters, content, context);
}
/**
* Execute provided script and return {@link Block} based result.
*
* @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 MacroExecutionException failed to evaluate provided content.
* @since 2.4M2
*/
protected List<Block> evaluateBlock(P parameters, String content, MacroTransformationContext context)
throws MacroExecutionException
{
String scriptResult = evaluateString(parameters, content, context);
List<Block> result = Collections.emptyList();
if (parameters.isOutput()) {
// Run the wiki syntax parser on the script-rendered content
result = parseScriptResult(scriptResult, parameters, context);
}
return result;
}
/**
* Parse provided content with the parser of the current wiki syntax.
*
* @param content the content to parse.
* @param context the context of the macro transformation.
* @return the result of the parsing.
* @throws MacroExecutionException failed to parse content
*/
protected List<Block> parseSourceSyntax(String content, MacroTransformationContext context)
throws MacroExecutionException
{
return this.contentParser.parse(content, context, false, false).getChildren();
}
}