org.eclipse.m2e.editor.xml.PomContentAssistProcessor.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.m2e.editor.xml.PomContentAssistProcessor.java

Source

/*******************************************************************************
 * Copyright (c) 2008-2010 Sonatype, Inc.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *      Sonatype, Inc. - initial API and implementation
 *******************************************************************************/

package org.eclipse.m2e.editor.xml;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import java.util.TreeSet;

import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.Text;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.runtime.IPath;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.ITextSelection;
import org.eclipse.jface.text.ITextViewer;
import org.eclipse.jface.text.Region;
import org.eclipse.jface.text.contentassist.CompletionProposal;
import org.eclipse.jface.text.contentassist.ICompletionProposal;
import org.eclipse.jface.text.source.ISourceViewer;
import org.eclipse.jface.text.templates.ContextTypeRegistry;
import org.eclipse.jface.text.templates.DocumentTemplateContext;
import org.eclipse.jface.text.templates.Template;
import org.eclipse.jface.text.templates.TemplateContext;
import org.eclipse.jface.text.templates.TemplateContextType;
import org.eclipse.jface.text.templates.TemplateException;
import org.eclipse.jface.text.templates.TemplateProposal;
import org.eclipse.jface.text.templates.persistence.TemplateStore;
import org.eclipse.osgi.util.NLS;
import org.eclipse.swt.graphics.Image;
import org.eclipse.ui.ISharedImages;
import org.eclipse.ui.PlatformUI;
import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion;
import org.eclipse.wst.sse.core.utils.StringUtils;
import org.eclipse.wst.xml.ui.internal.contentassist.ContentAssistRequest;
import org.eclipse.wst.xml.ui.internal.contentassist.XMLContentAssistProcessor;

import org.apache.maven.project.MavenProject;

import org.eclipse.m2e.core.MavenPlugin;
import org.eclipse.m2e.core.project.IMavenProjectFacade;
import org.eclipse.m2e.editor.xml.internal.Messages;
import org.eclipse.m2e.editor.xml.internal.XmlUtils;

/**
 * @author Lukas Krecan
 * @author Eugene Kuleshov
 */
@SuppressWarnings("restriction")
public class PomContentAssistProcessor extends XMLContentAssistProcessor {

    private static final ProposalComparator PROPOSAL_COMPARATOR = new ProposalComparator();

    private ISourceViewer sourceViewer;

    public PomContentAssistProcessor(ISourceViewer sourceViewer) {
        this.sourceViewer = sourceViewer;
    }

    //broken

    protected void addTagNameProposals(ContentAssistRequest contentAssistRequest, int childPosition) {
        String currentNodeName = getCurrentNode(contentAssistRequest).getNodeName();
        PomTemplateContext context = PomTemplateContext.fromNodeName(currentNodeName);
        if (PomTemplateContext.CONFIGURATION == context) {
            //this is sort of hack that makes sure the config proposals appear even
            // when you type <prefix
            // the downside is that additional typing hides the proposals popup
            // there has to be a better way though. the xml element completions seems to be coping with it fine..
            contentAssistRequest
                    .setReplacementBeginPosition(contentAssistRequest.getReplacementBeginPosition() - 1);
            contentAssistRequest.setReplacementLength(contentAssistRequest.getReplacementLength() + 1);
            addProposals(contentAssistRequest, context, getCurrentNode(contentAssistRequest),
                    contentAssistRequest.getMatchString());
        }
        if (PomTemplateContext.UNKNOWN == context) {
            context = PomTemplateContext
                    .fromNodeName(getCurrentNode(contentAssistRequest).getParentNode().getNodeName());
            if (PomTemplateContext.CONFIGURATION == context) {
                addProposals(contentAssistRequest, context, getCurrentNode(contentAssistRequest).getParentNode(),
                        contentAssistRequest.getMatchString());
            }
        }
        super.addTagNameProposals(contentAssistRequest, childPosition);
    }

    @Override
    protected void addTagInsertionProposals(ContentAssistRequest contentAssistRequest, int childPosition) {
        String currentNodeName = getCurrentNode(contentAssistRequest).getNodeName();

        addProposals(contentAssistRequest, PomTemplateContext.fromNodeName(currentNodeName));
        super.addTagInsertionProposals(contentAssistRequest, childPosition);
    }

    private Node getCurrentNode(ContentAssistRequest contentAssistRequest) {
        Node currentNode = contentAssistRequest.getNode();
        if (currentNode instanceof Text) {
            currentNode = currentNode.getParentNode();
        }
        return currentNode;
    }

    private void addProposals(ContentAssistRequest request, PomTemplateContext context) {
        ITextSelection selection = (ITextSelection) sourceViewer.getSelectionProvider().getSelection();
        int offset = request.getReplacementBeginPosition();
        // adjust offset to end of normalized selection
        if (selection.getOffset() == offset) {
            offset = selection.getOffset() + selection.getLength();
        }

        String prefix = extractPrefix(sourceViewer, offset);

        addExpressionProposal(request, context, getCurrentNode(request), prefix);

        addGenerateProposals(request, context, getCurrentNode(request), prefix);

        addProposals(request, context, getCurrentNode(request), prefix);
    }

    /**
     * this is a proposal method for adding expressions when ${ is typed..
     * 
     * @param request
     * @param context
     * @param currentNode
     * @param prefix
     */
    private void addExpressionProposal(ContentAssistRequest request, PomTemplateContext context, Node currentNode,
            String prefix) {
        int exprStart = prefix.lastIndexOf("${"); //$NON-NLS-1$
        if (exprStart != -1) {
            //the regular prefix is separated by whitespace and <> brackets only, we need to cut the last portion
            String realExpressionPrefix = prefix.substring(exprStart);
            if (realExpressionPrefix.contains("}")) { //$NON-NLS-1$
                //the expression is not opened..
                return;
            }
            if (expressionproposalContexts.contains(context)) {
                //add all effective pom expressions
                MavenProject prj = XmlUtils.extractMavenProject(sourceViewer);
                Region region = new Region(request.getReplacementBeginPosition() - realExpressionPrefix.length(),
                        realExpressionPrefix.length());
                Set<String> collect = new TreeSet<String>();
                if (prj != null) {
                    Properties props = prj.getProperties();
                    if (props != null) {
                        for (Object key : props.keySet()) {
                            String keyString = key.toString();
                            if (("${" + keyString).startsWith(realExpressionPrefix)) { //$NON-NLS-1$
                                collect.add(keyString);
                            }
                        }
                    }
                }

                //add a few hardwired values as well
                if ("${basedir}".startsWith(realExpressionPrefix)) { //$NON-NLS-1$
                    collect.add("basedir"); //$NON-NLS-1$
                }
                if ("${project.version}".startsWith(realExpressionPrefix)) { //$NON-NLS-1$
                    collect.add("project.version"); //$NON-NLS-1$
                }
                if ("${project.groupId}".startsWith(realExpressionPrefix)) { //$NON-NLS-1$
                    collect.add("project.groupId"); //$NON-NLS-1$
                }
                if ("${project.artifactId}".startsWith(realExpressionPrefix)) { //$NON-NLS-1$
                    collect.add("project.artifactId"); //$NON-NLS-1$
                }
                if ("${project.build.directory}".startsWith(realExpressionPrefix)) { //$NON-NLS-1$
                    collect.add("project.build.directory"); //$NON-NLS-1$
                }
                for (String key : collect) {
                    ICompletionProposal proposal = new InsertExpressionProposal(region, key, prj);
                    if (request.shouldSeparate()) {
                        request.addMacro(proposal);
                    } else {
                        request.addProposal(proposal);
                    }
                }
            }
        }
    }

    private static List<PomTemplateContext> expressionproposalContexts = Arrays
            .asList(new PomTemplateContext[] { PomTemplateContext.ARTIFACT_ID, PomTemplateContext.CLASSIFIER,
                    //     PomTemplateContext.CONFIGURATION,
                    PomTemplateContext.GOAL, PomTemplateContext.GROUP_ID, PomTemplateContext.MODULE,
                    PomTemplateContext.PACKAGING, PomTemplateContext.PHASE, PomTemplateContext.PROPERTIES, //??
                    PomTemplateContext.SCOPE, PomTemplateContext.SYSTEM_PATH, PomTemplateContext.TYPE,
                    //     PomTemplateContext.VERSION, version is intentionally not included as we have specialized handling there.. 
                    PomTemplateContext.UNKNOWN //this one is both important and troubling.. but having a context for everything is weird.
    });

    private void addGenerateProposals(ContentAssistRequest request, PomTemplateContext context, Node node,
            String prefix) {
        if (prefix.trim().length() != 0) {
            //only provide these generate proposals when there is no prefix.
            return;
        }
        if (context == PomTemplateContext.PARENT && node.getNodeName().equals("parent")) { //$NON-NLS-1$
            Element parent = (Element) node;
            Element relPath = XmlUtils.findChild(parent, "relativePath"); //$NON-NLS-1$
            if (relPath == null) {
                //only show when no relpath already defined..
                String relative = findRelativePath(sourceViewer, parent);
                if (relative != null) {
                    Region region = new Region(request.getReplacementBeginPosition(), 0);
                    ICompletionProposal proposal = new CompletionProposal(
                            "<relativePath>" + relative + "</relativePath>", //$NON-NLS-1$ //$NON-NLS-2$
                            region.getOffset(), region.getLength(), 0, //
                            PlatformUI.getWorkbench().getSharedImages().getImage(ISharedImages.IMG_OBJ_ADD), //
                            NLS.bind(Messages.PomContentAssistProcessor_insert_relPath_title, relative), null,
                            null);
                    if (request.shouldSeparate()) {
                        request.addMacro(proposal);
                    } else {
                        request.addProposal(proposal);
                    }
                }
            }
        }
        if (context == PomTemplateContext.RELATIVE_PATH) {
            //completion in the text portion of relative path
            Element parent = (Element) node.getParentNode();
            if (parent != null && "parent".equals(parent.getNodeName())) { //$NON-NLS-1$
                String relative = findRelativePath(sourceViewer, parent);
                String textContent = XmlUtils.getTextValue(node);
                if (relative != null && !relative.equals(textContent)) {
                    Region region = new Region(request.getReplacementBeginPosition() - prefix.length(),
                            prefix.length());
                    if (request.getNode() instanceof IndexedRegion && request.getNode() instanceof Text) {
                        //for <relativePath>|</relativePath> the current node is the element node and not the text node
                        //only replace the text node content..
                        IndexedRegion index = (IndexedRegion) request.getNode();
                        region = new Region(index.getStartOffset(), index.getEndOffset() - index.getStartOffset());
                    }
                    ICompletionProposal proposal = new CompletionProposal(relative, region.getOffset(),
                            region.getLength(), 0,
                            PlatformUI.getWorkbench().getSharedImages().getImage(ISharedImages.IMG_OBJ_ADD),
                            NLS.bind(Messages.PomContentAssistProcessor_set_relPath_title, relative), null, null);
                    if (request.shouldSeparate()) {
                        request.addMacro(proposal);
                    } else {
                        request.addProposal(proposal);
                    }
                }
            }
        }
        if (context == PomTemplateContext.DEPENDENCIES || context == PomTemplateContext.PROFILE
                || context == PomTemplateContext.DEPENDENCY_MANAGEMENT || context == PomTemplateContext.PROJECT) {
            //now add the proposal for dependency inclusion
            Region region = new Region(request.getReplacementBeginPosition(), 0);
            InsertArtifactProposal.Configuration config = new InsertArtifactProposal.Configuration(
                    InsertArtifactProposal.SearchType.DEPENDENCY);
            config.setCurrentNode(node);

            ICompletionProposal proposal = new InsertArtifactProposal(sourceViewer, region, config);
            if (request.shouldSeparate()) {
                request.addMacro(proposal);
            } else {
                request.addProposal(proposal);
            }
        }

        if (context == PomTemplateContext.PLUGINS || context == PomTemplateContext.BUILD
                || context == PomTemplateContext.PLUGIN_MANAGEMENT || context == PomTemplateContext.PROJECT) {
            //now add the proposal for plugin inclusion
            Region region = new Region(request.getReplacementBeginPosition(), 0);
            InsertArtifactProposal.Configuration config = new InsertArtifactProposal.Configuration(
                    InsertArtifactProposal.SearchType.PLUGIN);
            config.setCurrentNode(node);

            ICompletionProposal proposal = new InsertArtifactProposal(sourceViewer, region, config);
            if (request.shouldSeparate()) {
                request.addMacro(proposal);
            } else {
                request.addProposal(proposal);
            }

        }
        //comes after dependency and plugin.. the dep and plugin ones are guessed to be more likely hits..
        if (context == PomTemplateContext.PROJECT) {
            //check if we have a parent defined..
            Node project = node;
            if (project != null && project instanceof Element) {
                Element parent = XmlUtils.findChild((Element) project, "parent"); //$NON-NLS-1$
                if (parent == null) {
                    //now add the proposal for parent inclusion
                    Region region = new Region(request.getReplacementBeginPosition(), 0);
                    Element groupId = XmlUtils.findChild((Element) project, "groupId"); //$NON-NLS-1$
                    String groupString = null;
                    if (groupId != null) {
                        groupString = XmlUtils.getTextValue(groupId);
                    }
                    InsertArtifactProposal.Configuration config = new InsertArtifactProposal.Configuration(
                            InsertArtifactProposal.SearchType.PARENT);
                    config.setInitiaSearchString(groupString);
                    ICompletionProposal proposal = new InsertArtifactProposal(sourceViewer, region, config);
                    if (request.shouldSeparate()) {
                        request.addMacro(proposal);
                    } else {
                        request.addProposal(proposal);
                    }
                }
            }
        }
        if ((context == PomTemplateContext.PROJECT && XmlUtils.findChild((Element) node, "licenses") == null)
                || context == PomTemplateContext.LICENSES) {
            Region region = new Region(request.getReplacementBeginPosition(), 0);
            ICompletionProposal proposal = new InsertSPDXLicenseProposal(sourceViewer, context, region);
            request.addProposal(proposal);
        }
    }

    private static String findRelativePath(ISourceViewer viewer, Element parent) {
        String groupId = XmlUtils.getTextValue(XmlUtils.findChild(parent, "groupId")); //$NON-NLS-1$
        String artifactId = XmlUtils.getTextValue(XmlUtils.findChild(parent, "artifactId")); //$NON-NLS-1$
        String version = XmlUtils.getTextValue(XmlUtils.findChild(parent, "version")); //$NON-NLS-1$
        return findRelativePath(viewer, groupId, artifactId, version);
    }

    public static String findRelativePath(ISourceViewer viewer, String groupId, String artifactId, String version) {
        if (groupId != null && artifactId != null && version != null) {
            IMavenProjectFacade facade = MavenPlugin.getMavenProjectRegistry().getMavenProject(groupId, artifactId,
                    version);
            if (facade != null) {
                //now add the proposal for relativePath
                IFile parentPomFile = facade.getPom();
                IPath path = parentPomFile.getLocation();
                //TODO we might not need the IPRoject instance at all..
                IProject prj = XmlUtils.extractProject(viewer);
                if (prj != null && path != null) {
                    IPath path2 = prj.getLocation();
                    IPath relative = path.makeRelativeTo(path2);
                    if (relative != path) {
                        return relative.toString();
                    }
                }
            }
        }
        return null;
    }

    private void addProposals(ContentAssistRequest request, PomTemplateContext context, Node currentNode,
            String prefix) {
        if (request != null) {
            MavenProject prj = XmlUtils.extractMavenProject(sourceViewer);
            IProject eclipseprj = XmlUtils.extractProject(sourceViewer);

            ICompletionProposal[] templateProposals = getTemplateProposals(prj, eclipseprj, sourceViewer,
                    request.getReplacementBeginPosition(), context.getContextTypeId(), currentNode, prefix);
            for (ICompletionProposal proposal : templateProposals) {
                if (request.shouldSeparate()) {
                    request.addMacro(proposal);
                } else {
                    request.addProposal(proposal);
                }
            }
        }
    }

    private ICompletionProposal[] getTemplateProposals(MavenProject project, IProject eclipseprj,
            ITextViewer viewer, int offset, String contextTypeId, Node currentNode, String prefix) {
        ITextSelection selection = (ITextSelection) viewer.getSelectionProvider().getSelection();

        // adjust offset to end of normalized selection
        if (selection.getOffset() == offset) {
            offset = selection.getOffset() + selection.getLength();
        }

        //    String prefix = extractPrefix(viewer, offset);
        Region region = new Region(offset - prefix.length(), prefix.length());
        TemplateContext context = createContext(viewer, region, contextTypeId);
        if (context == null) {
            return new ICompletionProposal[0];
        }

        // name of the selection variables {line, word}_selection 
        context.setVariable("selection", selection.getText()); //$NON-NLS-1$

        PomTemplateContext templateContext = PomTemplateContext.fromId(contextTypeId);

        // add the user defined templates - separate them from the rest of the templates
        // so that we know what they are and can assign proper icon to them.
        Image image = MvnImages.IMG_USER_TEMPLATE;
        List<TemplateProposal> matches = new ArrayList<TemplateProposal>();
        TemplateStore store = MvnIndexPlugin.getDefault().getTemplateStore();
        if (store != null) {
            Template[] templates = store.getTemplates(contextTypeId);
            for (Template template : templates) {
                TemplateProposal proposal = createProposalForTemplate(prefix, region, context, image, template,
                        true);
                if (proposal != null) {
                    matches.add(proposal);
                }
            }
        }
        if (templateContext == PomTemplateContext.CONFIGURATION) {
            image = MvnImages.IMG_PARAMETER;
        } else {
            //other suggestions from the templatecontext are to be text inside the element, not actual
            //elements..
            image = null;
        }

        Template[] templates = templateContext.getTemplates(project, eclipseprj, currentNode, prefix);
        for (Template template : templates) {
            TemplateProposal proposal = createProposalForTemplate(prefix, region, context, image, template, false);
            if (proposal != null) {
                matches.add(proposal);
            }
        }

        if (templateContext != PomTemplateContext.VERSION) {
            // versions are already sorted with o.a.m.artifact.versioning.ComparableVersion
            Collections.sort(matches, PROPOSAL_COMPARATOR);
        }

        return (ICompletionProposal[]) matches.toArray(new ICompletionProposal[matches.size()]);

    }

    private TemplateProposal createProposalForTemplate(String prefix, Region region, TemplateContext context,
            Image image, final Template template, boolean isUserTemplate) {
        try {
            context.getContextType().validate(template.getPattern());
            if (template.matches(prefix, context.getContextType().getId())) {
                if (isUserTemplate) {
                    //for templates defined by users, preserve the default behaviour..
                    return new TemplateProposal(template, context, region, image, getRelevance(template, prefix)) {
                        public String getAdditionalProposalInfo() {
                            return StringUtils.convertToHTMLContent(super.getAdditionalProposalInfo());
                        }
                    };
                } else {
                    return new TemplateProposal(template, context, region, image, getRelevance(template, prefix)) {
                        public String getAdditionalProposalInfo() {
                            return getTemplate().getDescription();
                        }

                        public String getDisplayString() {
                            return template.getName();
                        }
                    };
                }
            }
        } catch (TemplateException e) {
            // ignore
        }

        return null;
    }

    protected TemplateContext createContext(ITextViewer viewer, IRegion region, String contextTypeId) {
        TemplateContextType contextType = getContextType(viewer, region, contextTypeId);
        if (contextType != null) {
            IDocument document = viewer.getDocument();
            return new DocumentTemplateContext(contextType, document, region.getOffset(), region.getLength());
        }
        return null;
    }

    //TODO we should have different relevance for user defined templates and generated proposals..
    protected int getRelevance(Template template, String prefix) {
        if (template.getName().startsWith(prefix))
            return 90;
        return 0;
    }

    protected TemplateContextType getContextType(ITextViewer viewer, IRegion region, String contextTypeId) {
        ContextTypeRegistry registry = MvnIndexPlugin.getDefault().getTemplateContextRegistry();
        if (registry != null) {
            return registry.getContextType(contextTypeId);
        }
        return null;
    }

    public static final String extractPrefix(ITextViewer viewer, int offset) {
        int i = offset;
        IDocument document = viewer.getDocument();
        if (i > document.getLength()) {
            return ""; //$NON-NLS-1$
        }

        try {
            while (i > 0) {
                char ch = document.getChar(i - 1);
                if (ch == '>' || ch == '<' || ch == ' ' || ch == '\n' || ch == '\t') {
                    break;
                }
                i--;
            }
            return document.get(i, offset - i);
        } catch (BadLocationException e) {
            return ""; //$NON-NLS-1$
        }
    }

    static final class ProposalComparator implements Comparator<TemplateProposal> {
        public int compare(TemplateProposal o1, TemplateProposal o2) {
            int res = o2.getRelevance() - o1.getRelevance();
            if (res == 0) {
                res = o1.getDisplayString().compareTo(o2.getDisplayString());
            }
            return res;
        }
    }

}