lu.softec.xwiki.macro.internal.ClassRunnerMacro.java Source code

Java tutorial

Introduction

Here is the source code for lu.softec.xwiki.macro.internal.ClassRunnerMacro.java

Source

/*
 * 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 lu.softec.xwiki.macro.internal;

import java.io.StringReader;
import java.io.StringWriter;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.lang3.StringUtils;
import org.xwiki.bridge.DocumentAccessBridge;
import org.xwiki.component.annotation.Component;
import org.xwiki.component.manager.ComponentLookupException;
import org.xwiki.component.manager.ComponentManager;
import org.xwiki.container.Container;
import org.xwiki.container.servlet.ServletApplicationContext;
import org.xwiki.container.servlet.ServletRequest;
import org.xwiki.container.servlet.ServletResponse;
import org.xwiki.context.Execution;
import org.xwiki.model.EntityType;
import org.xwiki.model.reference.DocumentReference;
import org.xwiki.model.reference.DocumentReferenceResolver;
import org.xwiki.model.reference.EntityReferenceSerializer;
import org.xwiki.model.reference.EntityReferenceValueProvider;
import org.xwiki.rendering.block.Block;
import org.xwiki.rendering.block.XDOM;
import org.xwiki.rendering.macro.AbstractMacro;
import org.xwiki.rendering.macro.MacroExecutionException;
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 lu.softec.xwiki.classloader.ClassLoaderCache;
import lu.softec.xwiki.macro.ClassRunnerMacroParameters;

/**
 * Example Macro.
 */
@Component
@Named("classrunner")
@Singleton
public class ClassRunnerMacro extends AbstractMacro<ClassRunnerMacroParameters> {

    /**
     * The description of the macro.
     */
    private static final String DESCRIPTION = "Execute a new POJO by executing its run(Writer,XWikiContext) or run(Writer, Map, XWikiContext)";
    /**
     * Used to find the parser from syntax identifier.
     */
    @Inject
    private ComponentManager componentManager;
    /**
     * Used to get the current context that we clone if the users asks to execute the included page in its own context.
     */
    @Inject
    private Execution execution;
    /**
     * Used to get the request and response access
     */
    @Inject
    private Container container;
    /**
     * Used as a factory for classloader
     */
    @Inject
    private ClassLoaderCache loaderf;
    /**
     * Used to access document content and check view access right.
     */
    @Inject
    private DocumentAccessBridge documentAccessBridge;
    /**
     * Used to create appropriate constant document references
     */
    @Inject
    @Named("current")
    private EntityReferenceValueProvider currentProvider;
    /**
     * Used to resolve document references
     */
    @Inject
    @Named("current")
    private DocumentReferenceResolver<String> currentDocumentReferenceResolver;
    /**
     * Used to convert reference to string for older API
     */
    @Inject
    private EntityReferenceSerializer<String> E;
    /**
     * Used to clean result of the parser syntax.
     */
    private ParserUtils parserUtils = new ParserUtils();

    /**
     * Create and initialize the descriptor of the macro.
     */
    public ClassRunnerMacro() {
        super("ClassRunner", DESCRIPTION, ClassRunnerMacroParameters.class);
    }

    /**
     * {@inheritDoc}
     * 
     * @see org.xwiki.rendering.macro.Macro#supportsInlineMode()
     */
    public boolean supportsInlineMode() {
        return true;
    }

    /**
     * Allows overriding the Document Access Bridge used (useful for unit tests).
     * 
     * @param documentAccessBridge the new Document Access Bridge to use
     */
    public void setDocumentAccessBridge(DocumentAccessBridge documentAccessBridge) {
        this.documentAccessBridge = documentAccessBridge;
    }

    /**
     * {@inheritDoc}
     * 
     * @see org.xwiki.rendering.macro.Macro#execute(Object, String, MacroTransformationContext)
     */
    @SuppressWarnings("unchecked")
    public List<Block> execute(ClassRunnerMacroParameters parameters, String content,
            MacroTransformationContext context) throws MacroExecutionException {
        DocumentAccessHelper docHelper = new DocumentAccessHelper(documentAccessBridge, currentProvider,
                currentDocumentReferenceResolver, E);

        boolean showDetailedException = true;
        String executionResult;

        try {
            // Hide Exception to all except admins
            showDetailedException = docHelper.hasAdmin();

            String baseURL = parameters.getBaseURL();
            String key = parameters.getKey();

            // TODO: Ensure security when we know precisely the document source
            // if (!this.documentAccessBridge.hasProgrammingRights()) {
            // throw new MacroExecutionException("You must have programming rigths to use this macro.");
            // }

            if (!(this.container.getApplicationContext() instanceof ServletApplicationContext)) {
                throw new MacroExecutionException(
                        "This macro is currently implemented only for a Servlet Context.");
            }

            HttpServletRequest httpRequest = ((ServletRequest) container.getRequest()).getHttpServletRequest();
            HttpServletResponse httpResponse = ((ServletResponse) container.getResponse()).getHttpServletResponse();

            DocumentReference profile = docHelper.getProfile(httpRequest, httpResponse, key,
                    parameters.getProfile());

            Collection<URL> pkgUrls = new ArrayList<URL>();
            Collection<URL> dpkgUrls = new ArrayList<URL>();
            List<String> groupIds = new ArrayList<String>();

            docHelper.getPackageList(profile, baseURL, pkgUrls, dpkgUrls, groupIds);

            if (pkgUrls.isEmpty() && dpkgUrls.isEmpty()) {
                throw new MacroExecutionException(
                        "No package to load in " + profile + ", no chance to find a class to run!");
            }

            // Only display detailed stackTrace to user working on a debug version
            showDetailedException = docHelper.hasAdmin() || !dpkgUrls.isEmpty();

            Map<Object, Object> xcontext = (Map<Object, Object>) execution.getContext().getProperty("xwikicontext");

            // Place the name of the choosed Profile in the XWikiContext for access from velocity and the executed class
            if (xcontext != null) {
                xcontext.put(key, profile.getName());
            }

            DocumentReference currentDoc = this.documentAccessBridge.getCurrentDocumentReference();
            String currentDatabase = currentProvider.getDefaultValue(EntityType.WIKI);

            String className = parameters.getClassName();
            if (StringUtils.isEmpty(className)) {
                className = StringUtils.join(
                        new String[] { currentDoc.getLastSpaceReference().getName(), ".", currentDoc.getName() });
            }

            boolean prefixed = false;
            for (String groupId : groupIds) {
                if (className.startsWith(groupId)) {
                    prefixed = true;
                    break;
                }
            }

            if (!prefixed) {
                className = StringUtils
                        .join(new String[] { groupIds.get(0), ".", currentDatabase, ".", className });
            }

            ClassLoader loader = getClassLoader(pkgUrls, dpkgUrls);
            executionResult = execute(loader, className, parameters, xcontext);
        } catch (Exception e) {
            if (showDetailedException) {
                throw new MacroExecutionException("Server Internal Error", e);
            } else {
                executionResult = "\n{{html}}\n<span class=\"xwikirenderingerror\">Server Internal Error</span>\n{{/html}}\n";
            }
        }

        try {
            List<Block> result = Collections.emptyList();

            if (parameters.isOutput()) {
                XDOM parsedDom = parseSourceSyntax(executionResult, parameters.getParser(), context);

                result = parsedDom.getChildren();
                if (context.isInline()) {
                    this.parserUtils.removeTopLevelParagraph(result);
                }
            }

            return result;
        } catch (Exception e) {
            if (showDetailedException) {
                throw new MacroExecutionException("Server Internal Error", e);
            } else {
                throw new MacroExecutionException("Server Internal Error");
            }
        }
    }

    private ClassLoader getClassLoader(Collection<URL> pkgUrls, Collection<URL> dpkgUrls) {
        ClassLoader loader = Thread.currentThread().getContextClassLoader();
        if (!dpkgUrls.isEmpty()) {
            pkgUrls.addAll(dpkgUrls);
        }
        if (!pkgUrls.isEmpty()) {
            loader = loaderf.getURLClassLoader(pkgUrls.toArray(new URL[0]), loader, !dpkgUrls.isEmpty());
        }
        return loader;
    }

    protected String execute(ClassLoader loader, String className, ClassRunnerMacroParameters parameters,
            Object xcontext) throws MacroExecutionException {
        StringWriter stringWriter = new StringWriter();
        boolean hasArgs = (parameters.getRawProperties() != null);
        Map<String, Object> args = (hasArgs) ? parameters.getRawProperties() : new LinkedHashMap<String, Object>();

        Class<?> klass;
        try {
            klass = loader.loadClass(className);
        } catch (ClassNotFoundException e) {
            throw new MacroExecutionException(e.getMessage(), e);
        }

        Method[] methods = klass.getMethods();
        Method run1 = null;
        Method run2 = null;
        Method run3 = null;
        Method setContext1 = null;
        Method setContext2 = null;
        Method getParser = null;
        for (Method m : methods) {
            if (m.getName().equals("run")) {
                Class<?>[] types = m.getParameterTypes();
                if (types.length == 1 && types[0].isAssignableFrom(stringWriter.getClass())) {
                    run1 = m;
                } else if (types.length == 2 && types[0].isAssignableFrom(stringWriter.getClass())
                        && types[1].isAssignableFrom(xcontext.getClass())) {
                    run2 = m;
                } else if (types.length == 3 && types[0].isAssignableFrom(stringWriter.getClass())
                        && types[1].isAssignableFrom(args.getClass())
                        && types[2].isAssignableFrom(xcontext.getClass())) {
                    run3 = m;
                }
            } else if (m.getName().equals("setContext")) {
                Class<?>[] types = m.getParameterTypes();
                if (types.length == 1 && types[0].isAssignableFrom(xcontext.getClass())) {
                    setContext1 = m;
                } else if (types.length == 2 && types[0].isAssignableFrom(args.getClass())
                        && types[1].isAssignableFrom(xcontext.getClass())) {
                    setContext2 = m;
                }
            } else if (m.getName().equals("getParser")) {
                Class<?>[] types = m.getParameterTypes();
                if (types.length == 0) {
                    getParser = m;
                }
            }
            if (run2 != null && run3 != null && setContext1 != null && setContext2 != null && getParser != null)
                break;
        }

        if (run2 == null && run3 == null) {
            throw new MacroExecutionException(
                    "Unable to find the appropriate run(Writer,XWikiContext) or run(Writer,Map,XWikiContext) method in the class.");
        }

        try {
            Object obj = klass.newInstance();
            if (setContext2 != null && (hasArgs || setContext1 == null)) {
                setContext2.invoke(obj, args, xcontext);
            } else if (setContext1 != null) {
                setContext1.invoke(obj, xcontext);
            }
            if (setContext2 != null || setContext1 != null) {
                if (getParser != null) {
                    String parser = null;
                    parser = (String) getParser.invoke(obj);
                    if (parser != null) {
                        parameters.setParser(parser);
                    }
                }
                run1.invoke(obj, stringWriter);
            } else {
                if (run3 != null && (hasArgs || run2 == null)) {
                    run3.invoke(obj, stringWriter, args, xcontext);
                } else {
                    run2.invoke(obj, stringWriter, xcontext);
                }
            }
        } catch (Exception e) {
            throw new MacroExecutionException(e.getMessage(), e);
        }

        return (stringWriter.toString());
    }

    /**
     * Get the parser of the current wiki syntax.
     *
     * @param parserId the parser Id to retrieve or null for the current syntax parser
     * @param context the context of the macro transformation.
     * @return the parser of the current wiki syntax.
     * @throws MacroExecutionException Failed to find source parser.
     */
    protected Parser getSyntaxParser(String parserId, MacroTransformationContext context)
            throws MacroExecutionException {
        try {
            return (Parser) this.componentManager.lookup(Parser.class,
                    (StringUtils.isNotEmpty(parserId)) ? parserId : context.getSyntax().toIdString());
        } catch (ComponentLookupException e) {
            throw new MacroExecutionException("Failed to find source parser", e);
        }
    }

    /**
     * Parse provided content with the parser of the current wiki syntax.
     * 
     * @param content the content to parse.
     * @param parserId the parser Id of the parser to use
     * @param context the context of the macro transformation.
     * @return an XDOM containing the parser content.
     * @throws MacroExecutionException failed to parse content
     */
    protected XDOM parseSourceSyntax(String content, String parserId, MacroTransformationContext context)
            throws MacroExecutionException {
        Parser parser = getSyntaxParser(parserId, context);

        try {
            return parser.parse(new StringReader(content));
        } catch (ParseException e) {
            throw new MacroExecutionException(
                    "Failed to parse content [" + content + "] with Syntax parser [" + parser.getSyntax() + "]", e);
        }
    }
}