blob: fa653f2fe0318c3f0f84bfbc9271b2ea71c02560 [file] [log] [blame]
/*******************************************************************************
* 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.EnumSet;
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.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.sse.ui.contentassist.CompletionProposalInvocationContext;
import org.eclipse.wst.xml.ui.internal.contentassist.ContentAssistRequest;
import org.eclipse.wst.xml.ui.internal.contentassist.DefaultXMLCompletionProposalComputer;
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 DefaultXMLCompletionProposalComputer {
private static Set<PomTemplateContext> expressionproposalContexts = EnumSet.of( //
PomTemplateContext.GROUP_ID, PomTemplateContext.ARTIFACT_ID, //
//version is intentionally not included as we have specialized handling there..
//PomTemplateContext.VERSION,
PomTemplateContext.PACKAGING, PomTemplateContext.TYPE, //
PomTemplateContext.CLASSIFIER, PomTemplateContext.SCOPE, PomTemplateContext.SYSTEM_PATH, //
PomTemplateContext.PROPERTIES, PomTemplateContext.MODULE, //
PomTemplateContext.PHASE, PomTemplateContext.GOAL, PomTemplateContext.CONFIGURATION, //
PomTemplateContext.SOURCEDIRECTORY, PomTemplateContext.SCRIPTSOURCEDIRECTORY,
PomTemplateContext.TESTSOURCEDIRECTORY, //
PomTemplateContext.OUTPUTDIRECTORY, PomTemplateContext.TESTOUTPUTDIRECTORY, //
PomTemplateContext.DIRECTORY, PomTemplateContext.FILTER, //
//this one is both important and troubling.. but having a context for everything is weird.
PomTemplateContext.UNKNOWN);
private static List<String> hardwiredProperties = Collections.unmodifiableList(Arrays.asList( //
"basedir", "project.basedir", //
"project.version", "project.groupId", "project.artifactId", "project.version", "project.name", //
"project.build.directory", "project.build.outputDirectory"));
protected void addTagNameProposals(ContentAssistRequest contentAssistRequest, int childPosition,
CompletionProposalInvocationContext ctx) {
PomTemplateContext context = PomTemplateContext.fromNode(contentAssistRequest.getParent());
if(PomTemplateContext.CONFIGURATION == context) {
addTemplateProposals(contentAssistRequest, context, ctx.getViewer(), true);
}
}
protected void addTagInsertionProposals(ContentAssistRequest contentAssistRequest, int childPosition,
CompletionProposalInvocationContext ctx) {
PomTemplateContext context = PomTemplateContext.fromNode(contentAssistRequest.getParent());
// wst content assist doesn't provide matchString in text content
int offset = contentAssistRequest.getReplacementBeginPosition();
String prefix = extractPrefix(ctx.getViewer(), offset);
contentAssistRequest.setMatchString(prefix);
contentAssistRequest.setReplacementBeginPosition(offset - prefix.length());
contentAssistRequest.setReplacementLength(prefix.length());
addExpressionProposals(contentAssistRequest, context, ctx.getViewer());
addGenerateProposals(contentAssistRequest, context, ctx.getViewer());
addTemplateProposals(contentAssistRequest, context, ctx.getViewer(), false);
}
/**
* this is a proposal method for adding expressions when ${ is typed..
*
* @param request
* @param context
* @param currentNode
* @param prefixPath
*/
private void addExpressionProposals(ContentAssistRequest request, PomTemplateContext context,
ITextViewer sourceViewer) {
String prefix = request.getMatchString();
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() + exprStart, realExpressionPrefix.length());
Set<String> collect = new TreeSet<String>();
String currentProp = null;
Node node = request.getParent();
if(PomTemplateContext.getAncestor(node, "properties", "project") != null
|| PomTemplateContext.getAncestor(node, "properties", "profile", "profiles", "project") != null) {
currentProp = node.getLocalName();
}
if(prj != null) {
Properties props = prj.getProperties();
if(props != null) {
for(Object key : props.keySet()) {
String keyString = key.toString();
if(keyString.equals(currentProp)) {
// do not allow recursive property usage
continue;
}
if(("${" + keyString).startsWith(realExpressionPrefix)) { //$NON-NLS-1$
collect.add(keyString);
}
}
}
}
//add a few hardwired values as well
for(String prop : hardwiredProperties) {
if(("${" + prop).startsWith(realExpressionPrefix)) { //$NON-NLS-1$
collect.add(prop);
}
}
for(String key : collect) {
request.addProposal(new InsertExpressionProposal(region, key, prj));
}
}
}
}
private void addGenerateProposals(ContentAssistRequest request, PomTemplateContext context,
ITextViewer sourceViewer) {
String prefix = request.getMatchString();
if(prefix.trim().length() != 0) {
//only provide these generate proposals when there is no prefix.
return;
}
Node node = request.getParent();
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);
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);
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);
request.addProposal(new InsertArtifactProposal(sourceViewer, region, config));
}
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);
request.addProposal(new InsertArtifactProposal(sourceViewer, region, config));
}
//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);
request.addProposal(new InsertArtifactProposal(sourceViewer, region, config));
}
}
}
if((context == PomTemplateContext.PROJECT && XmlUtils.findChild((Element) node, "licenses") == null)
|| context == PomTemplateContext.LICENSES) {
Region region = new Region(request.getReplacementBeginPosition(), 0);
request.addProposal(new InsertSPDXLicenseProposal(sourceViewer, context, region));
}
}
private static String findRelativePath(ITextViewer 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(ITextViewer 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 addTemplateProposals(ContentAssistRequest request, PomTemplateContext context, ITextViewer sourceViewer,
boolean tagProposals) {
MavenProject prj = XmlUtils.extractMavenProject(sourceViewer);
IProject eclipseprj = XmlUtils.extractProject(sourceViewer);
ITextSelection selection = (ITextSelection) sourceViewer.getSelectionProvider().getSelection();
Node parentNode = request.getParent();
int offset = request.getReplacementBeginPosition();
String prefix = request.getMatchString();
int len = prefix.length();
// replace text until the next whitespace or tag end
IndexedRegion ir = (IndexedRegion) request.getNode();
if(ir instanceof Text) {
IDocument document = sourceViewer.getDocument();
for(int i = offset + len; i < ir.getEndOffset(); i++ ) {
try {
if(Character.isWhitespace(document.getChar(i))) {
break;
}
} catch(BadLocationException e) {
break;
}
len++ ;
}
}
// also replace opening '<'
if(tagProposals) {
offset-- ;
len++ ;
}
Region region = new Region(offset, len);
TemplateContext templateContext = createContext(sourceViewer, region, context.getContextTypeId());
if(templateContext == null) {
return;
}
// name of the selection variables {line, word}_selection
templateContext.setVariable("selection", selection.getText()); //$NON-NLS-1$
// 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.
List<TemplateProposal> matches = new ArrayList<TemplateProposal>();
TemplateStore store = MvnIndexPlugin.getDefault().getTemplateStore();
if(store != null) {
Template[] templates = store.getTemplates(context.getContextTypeId());
for(Template template : templates) {
TemplateProposal proposal = createProposalForTemplate(prefix, region, templateContext,
MvnImages.IMG_USER_TEMPLATE, template, true);
if(proposal != null) {
matches.add(proposal);
}
}
}
Template[] templates = context.getTemplates(prj, eclipseprj, parentNode, prefix);
for(Template template : templates) {
Image image = null;
if(template instanceof PomTemplate) {
image = ((PomTemplate) template).getImage();
}
TemplateProposal proposal = createProposalForTemplate(prefix, region, templateContext, image, template, false);
if(proposal != null) {
matches.add(proposal);
}
}
for(ICompletionProposal proposal : matches) {
request.addProposal(proposal);
}
}
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 PomTemplateProposal(template, context, region, image, getRelevance(template, prefix)) {
public String getAdditionalProposalInfo() {
return StringUtils.convertToHTMLContent(super.getAdditionalProposalInfo());
}
};
}
return new PomTemplateProposal(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 instanceof PomTemplate) {
int rel = ((PomTemplate) template).getRelevance();
if(rel != -1)
return rel;
}
if(template.getName().startsWith(prefix))
return 1900;
return 1500;
}
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$
}
}
}