/******************************************************************************* * Copyright (c) 2012 Red Hat, Inc. * Distributed under license by Red Hat, Inc. All rights reserved. * This program is 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: * Red Hat, Inc. - initial API and implementation ******************************************************************************/ package org.jboss.tools.cdi.seam.config.ui.contentassist; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import org.eclipse.core.filebuffers.FileBuffers; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IProject; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.Path; import org.eclipse.jdt.core.IClassFile; import org.eclipse.jdt.core.ICompilationUnit; import org.eclipse.jdt.core.IField; import org.eclipse.jdt.core.IJavaElement; import org.eclipse.jdt.core.IJavaProject; import org.eclipse.jdt.core.IMember; import org.eclipse.jdt.core.IMethod; import org.eclipse.jdt.core.IPackageFragment; import org.eclipse.jdt.core.IPackageFragmentRoot; import org.eclipse.jdt.core.IType; import org.eclipse.jdt.core.JavaModelException; import org.eclipse.jdt.internal.ui.text.javadoc.JavadocContentAccess2; import org.eclipse.jface.text.IDocument; import org.eclipse.wst.sse.core.StructuredModelManager; import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel; import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocumentRegion; import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegion; import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegionList; import org.eclipse.wst.sse.ui.contentassist.CompletionProposalInvocationContext; import org.eclipse.wst.sse.ui.internal.contentassist.ContentAssistUtils; import org.eclipse.wst.sse.ui.internal.contentassist.CustomCompletionProposal; import org.eclipse.wst.xml.core.internal.contentmodel.CMNode; import org.eclipse.wst.xml.core.internal.provisional.document.IDOMElement; import org.eclipse.wst.xml.core.internal.provisional.document.IDOMNode; import org.eclipse.wst.xml.core.internal.regions.DOMRegionContext; import org.eclipse.wst.xml.ui.internal.contentassist.AbstractXMLModelQueryCompletionProposalComputer; import org.eclipse.wst.xml.ui.internal.contentassist.ContentAssistRequest; import org.eclipse.wst.xml.ui.internal.contentassist.XMLContentModelGenerator; import org.eclipse.wst.xml.ui.internal.contentassist.XMLRelevanceConstants; import org.eclipse.wst.xml.ui.internal.editor.XMLEditorPluginImageHelper; import org.eclipse.wst.xml.ui.internal.editor.XMLEditorPluginImages; import org.jboss.tools.cdi.core.CDICoreNature; import org.jboss.tools.cdi.core.CDICorePlugin; import org.jboss.tools.cdi.seam.config.core.CDISeamConfigConstants; import org.jboss.tools.cdi.seam.config.core.CDISeamConfigExtension; import org.jboss.tools.cdi.seam.config.core.util.Util; import org.jboss.tools.cdi.seam.config.core.xml.SAXElement; import org.jboss.tools.cdi.seam.config.ui.CDISeamConfigUIPlugin; import org.jboss.tools.cdi.seam.config.ui.CDISeamConfigUiImages; import org.jboss.tools.common.model.util.EclipseJavaUtil; import org.jboss.tools.common.model.util.EclipseResourceUtil; import org.jboss.tools.common.xml.XMLUtilities; import org.w3c.dom.Element; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; /** * * @author Viacheslav Kabanovich * */ public class SeamConfigXmlCompletionProposalComputer extends AbstractXMLModelQueryCompletionProposalComputer implements CDISeamConfigConstants { static int RELEVANCE_TAG = XMLRelevanceConstants.R_STRICTLY_VALID_TAG_NAME + 100; static int RELEVANCE_TAG_KEYWORD = RELEVANCE_TAG - 1; static int RELEVANCE_TAG_ANNOTATION = RELEVANCE_TAG_KEYWORD - 1; static int RELEVANCE_TAG_MEMBER = RELEVANCE_TAG_ANNOTATION - 1; static int RELEVANCE_TAG_TYPE = RELEVANCE_TAG_MEMBER - 1; CompletionProposalInvocationContext context; Node currentNode; Element parentElement; SAXElement sax; IFile resource; IProject project; IJavaProject javaProject; CDICoreNature cdi; CDISeamConfigExtension extension; Map<String, String> uriByPrefix = new HashMap<String, String>(); Map<String, String> prefixByUri = new HashMap<String, String>(); String eePrefix; Map<String, String> prefixByPackage = new HashMap<String, String>(); public List<?> computeCompletionProposals( CompletionProposalInvocationContext context, IProgressMonitor monitor) { this.context = context; currentNode = findNode(); if(currentNode instanceof IDOMElement && context.getInvocationOffset() == ((IDOMElement)currentNode).getStartOffset()) { currentNode = currentNode.getPreviousSibling() != null ? currentNode.getPreviousSibling() : currentNode.getParentNode(); } Node p = currentNode; while(p != null && !(p instanceof Element)) p = p.getParentNode(); if(!(p instanceof Element)) { // can do nothing without context of parent element. return new ArrayList<Object>(); } parentElement = (Element)p; sax = buildSAXElement(currentNode); fillNameSpaces(currentNode); resource = findResource(getDocument()); if(resource != null) { javaProject = EclipseResourceUtil.getJavaProject(resource.getProject()); cdi = CDICorePlugin.getCDI(resource.getProject(), true); if(cdi != null) extension = CDISeamConfigExtension.getExtension(cdi); } if(!isActive()) { return new ArrayList<Object>(); } //compute the completion proposals return super.computeCompletionProposals(context, monitor); } protected boolean isActive() { //Cannot work without resource. //Have nothing to suggest without packages. //can only add in context of an element. return (resource != null && !prefixByPackage.isEmpty() && sax != null); } protected IDocument getDocument() { return context.getDocument(); } private Node findNode() { return (Node)ContentAssistUtils.getNodeAt(context.getViewer(), context.getInvocationOffset()); } private IFile findResource(IDocument document) { IStructuredModel sModel = StructuredModelManager.getModelManager().getExistingModelForRead(document); try { if (sModel != null) { String baseLocation = sModel.getBaseLocation(); IPath location = new Path(baseLocation).makeAbsolute(); return FileBuffers.getWorkspaceFileAtLocation(location); } } finally { if (sModel != null) { sModel.releaseFromRead(); } } return null; } private SAXElement buildSAXElement(Node node) { while(node != null && !(node instanceof Element)) { node = node.getParentNode(); } if(node != null) { SAXElement sax = new SAXElement(node); SAXElement parent = buildSAXElement(node.getParentNode()); sax.setParent(parent); return sax; } return null; } @Override protected void addTagInsertionProposals( ContentAssistRequest contentAssistRequest, int childPosition, CompletionProposalInvocationContext context) { Set<TagData> tagData = collectTagData(); for (TagData tag: tagData) { if(tag.isUnique() && childOtherThanCurrentNodeExists(tag.getName())) { continue; } String tagText = tag.getText(); int positionAdjustment = (tagText.indexOf("><") > 0) ? tagText.indexOf("><") + 1 : tagText.length(); String displayText = tag.getName(); if(tag.getMember() instanceof IType) { displayText += " - " + ((IType)tag.getMember()).getPackageFragment().getElementName(); } createProposal(contentAssistRequest, tagText, displayText, positionAdjustment, tag.getMember(), tag.getRelevance()); } } private boolean childOtherThanCurrentNodeExists(String tagName) { Element element = XMLUtilities.getUniqueChild((Element)parentElement, tagName); return element != null && element != currentNode; } private Set<TagData> collectTagData() { String parentElementName = sax.getLocalName(); String parentElementPrefix = sax.getPrefix(); Set<TagData> tagData = new HashSet<TagData>(); if("beans".equals(parentElementName)) { //suggest all classes in all packages. addTypeNames(tagData, false, false); } else if(parentElementPrefix != null && prefixByPackage.containsValue(parentElementPrefix)) { // If we are not in <beans>, then we have to be in context of some seam package. IType contextType = null; if(Util.isEntry(sax)) { //Inside entry - only <key> and <value> if(eePrefix != null) { addTagData(tagData, eePrefix, KEYWORD_KEY, true, true, RELEVANCE_TAG_KEYWORD); addTagData(tagData, eePrefix, KEYWORD_VALUE, true, true, RELEVANCE_TAG_KEYWORD); } } else if(Util.isValue(sax) || Util.isKey(sax)) { //Inside value or key we can define new bean. addTypeNames(tagData, false, true); } else if(Util.isParameters(sax)) { //TODO find parent method name and type, and suggest parameter types. } else if((contextType = Util.resolveType(sax, cdi)) != null) { //We are inside bean. Suggest members and annotations. addTypeNames(tagData, true, false); //only annotations allowed here. addTagData(tagData, getTagNamesForMembers(parentElementPrefix, contextType, false), RELEVANCE_TAG_MEMBER); if(eePrefix != null) { if(!childOtherThanCurrentNodeExists(eePrefix + ":" + KEYWORD_MODIFIES) && !childOtherThanCurrentNodeExists(eePrefix + ":" + KEYWORD_REPLACES)) { addTagData(tagData, eePrefix, KEYWORD_MODIFIES, false, true, RELEVANCE_TAG_KEYWORD); addTagData(tagData, eePrefix, KEYWORD_REPLACES, false, true, RELEVANCE_TAG_KEYWORD); } addTagData(tagData, eePrefix, KEYWORD_PARAMETERS, true, true, RELEVANCE_TAG_KEYWORD); } } else if(sax.getParent() != null && ((contextType = Util.resolveType(sax.getParent(), cdi)) != null)) { IMember member = null; try { member = Util.resolveMember(contextType, sax); } catch (JavaModelException e) { CDISeamConfigUIPlugin.getDefault().logError(e); } if(member != null) { //We are inside bean member. Suggest annotations and <value>. addTypeNames(tagData, true, false); //only annotations allowed here. if(eePrefix != null) { if(member instanceof IField) { addTagData(tagData, eePrefix, KEYWORD_VALUE, true, RELEVANCE_TAG_KEYWORD); //should we suggest entry only for maps... addTagData(tagData, eePrefix, KEYWORD_ENTRY, true, true, RELEVANCE_TAG_KEYWORD); } else if(member instanceof IMethod) { addTagData(tagData, eePrefix, KEYWORD_PARAMETERS, true, true, RELEVANCE_TAG_KEYWORD); } } } } } return tagData; } private void addTypeNames(Set<TagData> tagData, boolean annotationsOnly, boolean classesOnly) { try { Map<String, IMember> tagNames = getAllTagNames(annotationsOnly, classesOnly); Set<TagData> append = new HashSet<TagData>(); addTagData(append, tagNames, annotationsOnly ? RELEVANCE_TAG_ANNOTATION : RELEVANCE_TAG_TYPE); if(annotationsOnly) { for (TagData tag: append) { tag.setHasClosingTag(false); } } tagData.addAll(append); } catch (JavaModelException e) { CDISeamConfigUIPlugin.getDefault().logError(e); } } private void addTagData(Set<TagData> tagData, String prefix, String name, boolean hasClosingTag, int relevance) { tagData.add(new TagData(prefix, name, hasClosingTag, false, relevance)); } private void addTagData(Set<TagData> tagData, String prefix, String name, boolean hasClosingTag, boolean isUnique, int relevance) { tagData.add(new TagData(prefix, name, hasClosingTag, isUnique, relevance)); } private void addTagData(Set<TagData> tagData, Map<String, IMember> tagNames, int relevance) { for (String tagName: tagNames.keySet()) { TagData d = new TagData(tagName, relevance); IMember m = tagNames.get(tagName); if(m instanceof IType) { String qn = ((IType)m).getFullyQualifiedName(); //TODO improve list of annotations that never need children. if(QUALIFIER_ANNOTATION_TYPE_NAME.equals(qn) || ANY_QUALIFIER_TYPE_NAME.equals(qn) || DEFAULT_QUALIFIER_TYPE_NAME.equals(qn) || SPECIALIZES_ANNOTATION_TYPE_NAME.equals(qn) || DECORATOR_STEREOTYPE_TYPE_NAME.equals(qn) || INJECT_ANNOTATION_TYPE_NAME.equals(qn) || PRODUCES_ANNOTATION_TYPE_NAME.equals(qn)) { d.setHasClosingTag(false); } } d.setMember(m); tagData.add(d); } } protected void addTagNameProposals(ContentAssistRequest contentAssistRequest, int childPosition, CompletionProposalInvocationContext context) { IDOMNode node = (IDOMNode) contentAssistRequest.getNode(); boolean isEnded = (node.getNodeType() != Node.TEXT_NODE) && node.getFirstStructuredDocumentRegion().isEnded(); if(currentNode == parentElement) { if(parentElement.getParentNode() instanceof Element) { parentElement = (Element)parentElement.getParentNode(); sax = sax.getParent(); } } String match = contentAssistRequest.getMatchString(); Set<TagData> tagData = collectTagData(); if(match != null && match.length() > 0) { Iterator<TagData> i = tagData.iterator(); while(i.hasNext()) { TagData d = i.next(); if(!d.getName().startsWith(match) && d.getName().indexOf(":" + match) < 0) { i.remove(); } } } for (TagData tag: tagData) { if(tag.isUnique() && childOtherThanCurrentNodeExists(tag.getName())) { continue; } String tagText = isEnded ? tag.getName() : tag.getText(); if(tagText.startsWith("<")) tagText = tagText.substring(1); int positionAdjustment = (tagText.indexOf("><") > 0) ? tagText.indexOf("><") + 1 : tagText.length(); String displayText = tag.getName(); if(tag.getMember() instanceof IType) { displayText += " - " + ((IType)tag.getMember()).getPackageFragment().getElementName(); } createProposal(contentAssistRequest, tagText, displayText, positionAdjustment, tag.getMember(), tag.getRelevance()); } } private void createProposal(ContentAssistRequest contentAssistRequest, String tagText, String displayText, int positionAdjustment, final IMember member, int relevance) { int begin = contentAssistRequest.getReplacementBeginPosition(); int length = contentAssistRequest.getReplacementLength(); String imagePath = XMLEditorPluginImages.IMG_OBJ_TAG_GENERIC; if(tagText.endsWith("\"")) { //improve that dirty hack imagePath = XMLEditorPluginImages.IMG_OBJ_ATTRIBUTE; } CustomCompletionProposal textProposal = new CustomCompletionProposal( tagText, begin, length, positionAdjustment, XMLEditorPluginImageHelper.getInstance().getImage(imagePath), displayText, null, null, relevance) { String proposedInfo = null; public String getAdditionalProposalInfo() { if(member != null && proposedInfo == null) { try { proposedInfo = JavadocContentAccess2.getHTMLContent(member, true); if(proposedInfo == null) proposedInfo = ""; } catch (JavaModelException e) { CDISeamConfigUIPlugin.getDefault().logError(e); } catch (CoreException e) { CDISeamConfigUIPlugin.getDefault().logError(e); } } return proposedInfo; } }; contentAssistRequest.addProposal(textProposal); } protected void addAttributeNameProposals( ContentAssistRequest contentAssistRequest, CompletionProposalInvocationContext context) { IType contextType = Util.resolveType(sax, cdi); if(contextType != null) { String match = contentAssistRequest.getMatchString(); Set<TagData> tagData = new HashSet<TagData>(); addTagData(tagData, getTagNamesForMembers(sax.getPrefix(), contextType, true), RELEVANCE_TAG_MEMBER); for (TagData tag: tagData) { String attrName = tag.getName().substring(tag.getName().indexOf(':') + 1); if(parentElement.hasAttribute(attrName)) continue; if(match != null && !attrName.startsWith(match)) continue; String tagText = attrName + "=\"\""; int positionAdjustment = tagText.length() - 1; createProposal(contentAssistRequest, tagText, attrName, positionAdjustment, tag.getMember(), tag.getRelevance()); } } } protected void addAttributeValueProposals( ContentAssistRequest contentAssistRequest, CompletionProposalInvocationContext context) { IDOMNode node = (IDOMNode) contentAssistRequest.getNode(); // Find the attribute region and name for which this position should // have a value proposed IStructuredDocumentRegion open = node.getFirstStructuredDocumentRegion(); ITextRegionList openRegions = open.getRegions(); int i = openRegions.indexOf(contentAssistRequest.getRegion()); if (i < 0) { return; } ITextRegion nameRegion = null; while (i >= 0) { nameRegion = openRegions.get(i--); if (nameRegion.getType() == DOMRegionContext.XML_TAG_ATTRIBUTE_NAME) { break; } } // the name region is REQUIRED to do anything useful if (nameRegion != null) { String attributeName = open.getText(nameRegion); if(attributeName.startsWith("xmlns:")) { String match = contentAssistRequest.getMatchString(); if(match == null) { match = ""; } else if(match.startsWith("\"") || match.startsWith("'")) { match = match.substring(1); } if(match.length() == 0 || match.startsWith(URI_PREFIX) || URI_PREFIX.startsWith(match)) { Set<String> packages = new HashSet<String>(); try { packages = getAllPackages(javaProject); } catch (JavaModelException e) { CDISeamConfigUIPlugin.getDefault().logError(e); } packages.add(PACKAGE_EE); for (String pkg: packages) { String proposal = "urn:java:" + pkg; if(match.startsWith(URI_PREFIX)) { int q = match.lastIndexOf(":"); String pMatch = match.substring(q + 1); if(!pkg.startsWith(pMatch)) { continue; } if(pkg.indexOf('.', pMatch.length() + 1) >= 0) { continue; } proposal = match.substring(0, q + 1) + pkg; } else { if(pkg.indexOf('.') > 0) continue; } int positionAdjustment = proposal.length(); String displayText = pkg; createValueProposal(contentAssistRequest, proposal, displayText, positionAdjustment, null, 1000); } } } } } private void createValueProposal(ContentAssistRequest contentAssistRequest, String tagText, String displayText, int positionAdjustment, IMember member, int relevance) { int begin = contentAssistRequest.getReplacementBeginPosition() + 1; String match = contentAssistRequest.getMatchString(); int length = match == null || match.length() == 0 ? 0 : match.length() - 1; String proposedInfo = null; CustomCompletionProposal textProposal = new CustomCompletionProposal( tagText, begin, length, positionAdjustment, CDISeamConfigUiImages.PACKAGE_IMAGE, displayText, null, proposedInfo, relevance) { // we make it our own type to filter from WTP's proposals }; contentAssistRequest.addProposal(textProposal); } @Override protected XMLContentModelGenerator getContentGenerator() { return new XMLContentModelGenerator(); } @Override protected boolean validModelQueryNode(CMNode node) { return false; } static String XMLNS_PREFIX = "xmlns:"; private void fillNameSpaces(Node node) { uriByPrefix.clear(); prefixByUri.clear(); prefixByPackage.clear(); eePrefix = null; while(node != null) { if(node instanceof Element) { Element element = (Element)node; NamedNodeMap as = element.getAttributes(); for (int i = 0; i < as.getLength(); i++) { Node a = as.item(i); String nm = a.getNodeName(); if(nm.startsWith(XMLNS_PREFIX)) { String prefix = nm.substring(XMLNS_PREFIX.length()); String uri = a.getNodeValue(); if(uri != null) { uriByPrefix.put(prefix, uri); prefixByUri.put(uri, prefix); String[] packages = Util.getPackages(uri); for (String pkg: packages) { prefixByPackage.put(pkg, prefix); } } } } } eePrefix = prefixByPackage.get(PACKAGE_EE); node = node.getParentNode(); } } Map<String, IMember> getAllTagNames(boolean annotationsOnly, boolean classesOnly) throws JavaModelException { Map<String, IMember> result = new HashMap<String, IMember>(); for (String packageName: prefixByPackage.keySet()) { String prefix = prefixByPackage.get(packageName); Map<String, IMember> typeNames = findTypeNamesByPackage(javaProject, packageName, annotationsOnly, classesOnly); for (String typeName: typeNames.keySet()) { result.put(prefix + ":" + typeName, typeNames.get(typeName)); } } return result; } Map<String, IMember> getTypeNamesByPrefix(String prefix, boolean annotationsOnly, boolean classesOnly) throws JavaModelException { Map<String, IMember> result = new HashMap<String, IMember>(); String uri = uriByPrefix.get(prefix); for (String packageName: Util.getPackages(uri)) { result.putAll(findTypeNamesByPackage(javaProject, packageName, annotationsOnly, classesOnly)); } return result; } public static Map<String, IMember> findTypeNamesByPackage(IJavaProject javaProject, String packageName, boolean annotationsOnly, boolean classesOnly) throws JavaModelException { Map<String, IMember> result = new HashMap<String, IMember>(); if(PACKAGE_EE.equals(packageName)) { for (String name: Util.EE_TYPES.keySet()) { String cls = Util.EE_TYPES.get(name); IType t = EclipseJavaUtil.findType(javaProject, cls); if(accept(t, annotationsOnly, classesOnly)) result.put(name, t); } for (String name: Util.EE_TYPES_30.keySet()) { String cls = Util.EE_TYPES_30.get(name); IType t = EclipseJavaUtil.findType(javaProject, cls); if(accept(t, annotationsOnly, classesOnly)) result.put(name, t); } } else if(javaProject != null) { IPackageFragmentRoot[] rs = javaProject.getAllPackageFragmentRoots(); for (IPackageFragmentRoot r: rs) { IPackageFragment pkg = r.getPackageFragment(packageName); if(pkg != null && pkg.exists()) { ICompilationUnit[] units = pkg.getCompilationUnits(); for (ICompilationUnit u: units) { IType[] ts = u.getTypes(); for (IType t: ts) if(accept(t, annotationsOnly, classesOnly)) result.put(t.getElementName(), t); } IClassFile[] cs = pkg.getClassFiles(); for (IClassFile cls: cs) { if(accept(cls.getType(), annotationsOnly, classesOnly)) result.put(cls.getType().getElementName(), cls.getType()); } } } } return result; } public static Set<String> getAllPackages(IJavaProject javaProject) throws JavaModelException { Set<String> result = new HashSet<String>(); IPackageFragmentRoot[] rs = javaProject.getAllPackageFragmentRoots(); for (IPackageFragmentRoot r: rs) { IJavaElement[] cs = r.getChildren(); for (IJavaElement c: cs) { if(c instanceof IPackageFragment) { result.add(((IPackageFragment)c).getElementName()); } } } return result; } private static boolean accept(IType type, boolean annotationOnly, boolean classesOnly) throws JavaModelException { return (type != null) && (!annotationOnly || type.isAnnotation() && (!classesOnly || !type.isInterface())); } private Map<String, IMember> getTagNamesForMembers(String prefix, IType type, boolean fieldsOnly) { Map<String, IMember> result = new HashMap<String, IMember>(); try { IField[] fs = type.getFields(); for (IField f: fs) { result.put(prefix + ":" + f.getElementName(), f); } if(!fieldsOnly || type.isAnnotation()) { IMethod[] ms = type.getMethods(); for (IMethod m: ms) { result.put(prefix + ":" + m.getElementName(), m); } } } catch (JavaModelException e) { CDISeamConfigUIPlugin.getDefault().logError(e); } return result; } }