org.xwiki.editor.tool.autocomplete.internal.AutoCompletionResource.java Source code

Java tutorial

Introduction

Here is the source code for org.xwiki.editor.tool.autocomplete.internal.AutoCompletionResource.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 org.xwiki.editor.tool.autocomplete.internal;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import javax.inject.Inject;
import javax.inject.Provider;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.QueryParam;

import org.apache.commons.lang3.StringUtils;
import org.apache.velocity.VelocityContext;
import org.slf4j.Logger;
import org.xwiki.component.annotation.Component;
import org.xwiki.component.manager.ComponentLookupException;
import org.xwiki.component.manager.ComponentManager;
import org.xwiki.editor.tool.autocomplete.AutoCompletionMethodFinder;
import org.xwiki.editor.tool.autocomplete.HintData;
import org.xwiki.editor.tool.autocomplete.Hints;
import org.xwiki.editor.tool.autocomplete.TargetContent;
import org.xwiki.editor.tool.autocomplete.TargetContentLocator;
import org.xwiki.editor.tool.autocomplete.TargetContentType;
import org.xwiki.model.reference.DocumentReference;
import org.xwiki.rest.XWikiRestComponent;
import org.xwiki.velocity.VelocityManager;
import org.xwiki.velocity.internal.util.InvalidVelocityException;
import org.xwiki.velocity.internal.util.VelocityParser;
import org.xwiki.velocity.internal.util.VelocityParserContext;

import com.xpn.xwiki.doc.XWikiDocument;
import com.xpn.xwiki.web.Utils;
import com.xpn.xwiki.XWikiContext;
import com.xpn.xwiki.api.Document;

/**
 * REST Resource for returning autocompletion hints. The content to autocomplete is passed in the request body, the
 * position of the cursor and the syntax in which the content is written in are passed as request parameters.
 *
 * @version $Id$
 */
@Component("org.xwiki.editor.tool.autocomplete.internal.AutoCompletionResource")
@Path("/autocomplete")
public class AutoCompletionResource implements XWikiRestComponent {
    /**
     * Used to get the Velocity Context from which we retrieve the list of bound variables that are used for
     * autocompletion.
     */
    @Inject
    private VelocityManager velocityManager;

    /**
     * Used to create fake documents and add "doc", "tdoc", and "sdoc" in the velocity context.
     */
    @Inject
    private Provider<XWikiContext> xcontextProvider;

    /**
     * Used to dynamically find Autocompletion Method finder to handle specific cases.
     *
     * @see AutoCompletionMethodFinder
     */
    @Inject
    private ComponentManager componentManager;

    /**
     * Used to autodiscover method hints.
     */
    @Inject
    private AutoCompletionMethodFinder defaultAutoCompletionMethodFinder;

    /**
     * Used to extract the content and the type under the cursor position.
     */
    @Inject
    private TargetContentLocator targetContentLocator;

    /**
     * Logging framework.
     */
    @Inject
    private Logger logger;

    /**
     * A Velocity Parser that we use to help parse Velocity content for figuring out autocompletion.
     */
    private VelocityParser parser = new VelocityParser();

    /**
     * Main REST entry point for getting Autocompletion hints.
     *
     * @param offset the position of the cursor in the full content
     * @param syntaxId the syntax in which the content is written in
     * @param content the full wiki content
     * @return the list of autocompletion hints
     */
    @POST
    public Hints getAutoCompletionHints(@QueryParam("offset") int offset, @QueryParam("syntax") String syntaxId,
            String content) {
        Hints hints = new Hints();

        // Only support autocompletion on Velocity ATM
        TargetContent targetContent = this.targetContentLocator.locate(content, syntaxId, offset);
        if (targetContent.getType() == TargetContentType.VELOCITY) {
            hints = getHints(targetContent.getContent(), targetContent.getPosition());
        }

        // Subtract the temporary user input size from the initial offset to get the absolute start offset of the user's
        // input.
        hints.withStartOffset(offset - hints.getStartOffset());

        return hints;
    }

    /**
     * Parses the passed content and return the autocompletion hints for the passed cursor position.
     *
     * @param content the Velocity content to autocomplete
     * @param offset the position of the cursor relative to the Velocity content
     * @return the list of autocompletion hints
     */
    private Hints getHints(String content, int offset) {
        Hints results = new Hints();

        // General algorithm:
        // - We start parsing at the first dollar before the cursor
        // - We get the full reference (a reference in VTL can be a variable, a property or a method call, see
        // http://velocity.apache.org/engine/devel/user-guide.html#References)
        // - We split the reference on "." and handle first the case of a variable. If there's no "." then it means
        // we're autocompleting a variable and we find all matching Velocity context variables and return them.
        // - If there's at least one "." then we parse the whole chain of method calls till the last dot to find the
        // return type of the last method call. This allows us to know the full list of methods for autocompletion.

        // Find the dollar sign before the current position
        char[] chars = content.toCharArray();
        VelocityContext velocityContext = getVelocityContext();
        int dollarPos = StringUtils.lastIndexOf(content, '$', offset);

        if (dollarPos == -1) {
            return results;
        }

        // Special case for when there's no variable after the dollar position since the Velocity Parser doesn't
        // support parsing this case.
        if (isCursorDirectlyAfterDollar(chars, dollarPos, offset)) {
            // Find all objects bound to the Velocity Context. We need to also look in the chained context since
            // this is where we store Velocity Tools
            results = getVelocityContextKeys("", velocityContext);
        } else {
            // The cursor is not directly after the dollar sign.
            try {
                // Get all the references after the dollar sign. For example if the input is "$a.b().ccc" then
                // we get "a.b().ccc".
                VelocityParserContext context = new VelocityParserContext();
                StringBuffer reference = new StringBuffer();
                StringBuffer identifier = new StringBuffer();
                int endPos = this.parser.getVar(chars, dollarPos, identifier, reference, context);

                // If endPos matches the current cursor position then it means we have a valid token for
                // autocompletion. Otherwise we don't autocomplete (for example there could be spaces between the
                // reference and the cursor position).
                // Note: We need to handle the special when the cursor is just after the '.' char.
                if (endPos + 1 == offset && chars[endPos] == '.') {
                    endPos++;
                    reference.append('.');
                }
                if (endPos == offset) {
                    // Find out if we're autocompleting a variable. In this case there's no "." in the reference
                    int methodPos = reference.indexOf(".");
                    if (methodPos > -1) {
                        // Autocomplete a method!
                        results = getHintsForMethodCall(chars, dollarPos + methodPos, identifier.toString());
                    } else {
                        // Autocomplete a variable! Find all matching variables.
                        results = getVelocityContextKeys(identifier.toString(), velocityContext);
                    }
                }
            } catch (InvalidVelocityException e) {
                this.logger.debug("Failed to get autocomplete hints for content [{}] at offset [{}]",
                        new Object[] { content, offset, e });
            }
        }

        return results;
    }

    /**
     * Find hints for the passed content assuming that it's representing method calls.
     *
     * @param chars the content to parse
     * @param currentPos the current position at which method calls are starting
     * @param variableName the name of the variable on which the first method is called
     * @return the list of autocompletion hints
     * @throws InvalidVelocityException if a parsing error occurs
     */
    private Hints getHintsForMethodCall(char[] chars, int currentPos, String variableName)
            throws InvalidVelocityException {
        Hints results = new Hints();
        VelocityParserContext context = new VelocityParserContext();

        // Find the next method after the currentPos.
        int pos = currentPos;

        // Handle the case when the variable of the first method call does not exist, also avoiding a NPE here.
        // See http://jira.xwiki.org/browse/WIKIEDITOR-18
        Object contextVariable = getVelocityContext().get(variableName);
        if (contextVariable == null) {
            return results;
        }

        AutoCompletionMethodFinder methodFinder = getMethodFinder(variableName);
        List<Class> methodClasses = Arrays.asList((Class) contextVariable.getClass());

        do {
            // Handle the special case when the cursor is after the dot ('.')
            String methodName;
            if (pos == chars.length - 1) {
                methodName = "";
                pos++;
            } else {
                StringBuffer method = new StringBuffer();
                pos = this.parser.getMethodOrProperty(chars, pos, method, context);
                methodName = StringUtils.substringBefore(method.toString(), "(").substring(1);
            }

            if (pos == chars.length) {
                // Find all methods matching methodName in methodClasses
                for (Class methodClass : methodClasses) {
                    results.withHints(methodFinder.findMethods(methodClass, methodName));
                }

                // Set the hints offset to be able to determine where the completion should be inserted.
                results = results.withStartOffset(methodName.length());

                break;
            } else {
                // Find the returned type for method "methodName".
                List<Class> returnTypes = new ArrayList<Class>();
                for (Class methodClass : methodClasses) {
                    returnTypes.addAll(methodFinder.findMethodReturnTypes(methodClass, methodName));
                }
                methodClasses = returnTypes;

                // Reset the method finder since we use a specialized finder only for the first autocompletion method
                methodFinder = this.defaultAutoCompletionMethodFinder;
            }
        } while (true);

        return results;
    }

    /**
     * @param chars the content to parse
     * @param dollarPos the position of the dollar symbol
     * @param offset the position in the whole content of the cursor
     * @return false if there's a velocity variable after the dollar sign
     */
    private boolean isCursorDirectlyAfterDollar(char[] chars, int dollarPos, int offset) {
        boolean result = true;

        for (int i = dollarPos; i < offset; i++) {
            if (chars[i] != '$' && chars[i] != '!' && chars[i] != '{') {
                result = false;
                break;
            }
        }

        return result;
    }

    /**
     * Find out all Velocity variable names bound in the Velocity Context.
     *
     * @param fragmentToMatch the prefix to filter with in order to return only variable whose names start with the
     *            passed string
     * @param velocityContext the Velocity Context from which to get the bound variables
     * @return the Velocity variables
     */
    private Hints getVelocityContextKeys(String fragmentToMatch, VelocityContext velocityContext) {
        Hints hints = new Hints();

        addVelocityKeys(hints, velocityContext.getKeys(), fragmentToMatch);
        if (velocityContext.getChainedContext() != null) {
            addVelocityKeys(hints, velocityContext.getChainedContext().getKeys(), fragmentToMatch);
        }

        // Set the hints offset to be able to determine where the completion should be inserted.
        hints = hints.withStartOffset(fragmentToMatch.length());

        return hints;
    }

    /**
     * Add variables to the passed results list.
     *
     * @param results the list of variable names
     * @param keys the keys containing the variables to add
     * @param fragmentToMatch the filter in order to only add variable whose names start with the passed string
     */
    private void addVelocityKeys(Hints results, Object[] keys, String fragmentToMatch) {
        for (Object key : keys) {
            if (key instanceof String && ((String) key).startsWith(fragmentToMatch)) {
                results.withHints(new HintData((String) key, (String) key));
            }
        }
    }

    /**
     * @param hint the hint of the finder to return. If no such component exist return the default finder
     * @return the {@link AutoCompletionMethodFinder} to use depending on the passed hint
     */
    private AutoCompletionMethodFinder getMethodFinder(String hint) {
        AutoCompletionMethodFinder finder = null;

        // Allow special handling for classes that have registered a custom introspection handler
        if (this.componentManager.hasComponent(AutoCompletionMethodFinder.class, hint)) {
            try {
                finder = this.componentManager.getInstance(AutoCompletionMethodFinder.class, hint);
            } catch (ComponentLookupException e) {
                // Component not found, continue with default finder...
            }
        }

        if (finder == null) {
            finder = this.defaultAutoCompletionMethodFinder;
        }

        return finder;
    }

    /**
     * @return the Velocity Context used to find existing bound variables
     */
    protected VelocityContext getVelocityContext() {
        VelocityContext velocityContext = this.velocityManager.getVelocityContext();
        XWikiContext context = this.xcontextProvider.get();

        // We add the "doc", "tdoc" and "sdoc" mappings since we don't get them from the Velocity Manager as they are
        // normally added based on the document passed in the request. However since we return the same method names
        // whatever the doc, we can manually add them.
        Document fakeDocument = createFakeXWikiDocument().newDocument(context);
        velocityContext.put("doc", fakeDocument);
        velocityContext.put("sdoc", fakeDocument);
        velocityContext.put("tdoc", fakeDocument);

        return velocityContext;
    }

    /**
     * @return a fake XWiki Document instance
     */
    protected XWikiDocument createFakeXWikiDocument() {
        // Note: Creating an XWikiDocument instance requires that the static component manager be set up
        // unfortunately...
        Utils.setComponentManager(this.componentManager);
        return new XWikiDocument(new DocumentReference("notusedwiki", "notusedspace", "notusedpage"));
    }
}