| /******************************************************************************* |
| * 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$ |
| } |
| } |
| |
| } |