Java tutorial
/*-****************************************************************************** * Copyright (c) 2014 Iwao AVE!. * 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: * Iwao AVE! - initial API and implementation and/or initial documentation *******************************************************************************/ package net.harawata.mybatipse.mybatis; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import javax.xml.xpath.XPathExpressionException; import net.harawata.mybatipse.Activator; import net.harawata.mybatipse.bean.BeanPropertyCache; import net.harawata.mybatipse.bean.BeanPropertyInfo; import net.harawata.mybatipse.mybatis.TypeAliasMap.TypeAliasInfo; import net.harawata.mybatipse.util.NameUtil; import net.harawata.mybatipse.util.XpathUtil; import org.eclipse.core.resources.IFile; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.Status; import org.eclipse.jdt.core.Flags; import org.eclipse.jdt.core.IJavaProject; import org.eclipse.jdt.core.IMethod; import org.eclipse.jdt.core.IType; import org.eclipse.jdt.core.JavaModelException; import org.eclipse.jdt.core.compiler.CharOperation; import org.eclipse.jdt.core.search.IJavaSearchConstants; import org.eclipse.jdt.core.search.IJavaSearchScope; import org.eclipse.jdt.core.search.SearchEngine; import org.eclipse.jdt.core.search.SearchMatch; import org.eclipse.jdt.core.search.SearchParticipant; import org.eclipse.jdt.core.search.SearchPattern; import org.eclipse.jdt.core.search.SearchRequestor; import org.eclipse.jdt.core.search.TypeNameRequestor; import org.eclipse.jdt.internal.core.PackageFragment; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.contentassist.CompletionProposal; import org.eclipse.jface.text.contentassist.ContextInformation; import org.eclipse.jface.text.contentassist.ICompletionProposal; 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.core.utils.StringUtils; import org.eclipse.wst.sse.ui.contentassist.CompletionProposalInvocationContext; import org.eclipse.wst.xml.core.internal.provisional.document.IDOMDocument; import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel; 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.ContentAssistRequest; import org.eclipse.wst.xml.ui.internal.contentassist.DefaultXMLCompletionProposalComputer; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; /** * @author Iwao AVE! */ @SuppressWarnings("restriction") public class XmlCompletionProposalComputer extends DefaultXMLCompletionProposalComputer { enum ProposalType { None, MapperNamespace, ResultType, ResultProperty, StatementId, TypeHandlerType, CacheType, ResultMap, Include, Package, TypeAlias, SelectId } @Override protected void addTagInsertionProposals(ContentAssistRequest contentAssistRequest, int childPosition, CompletionProposalInvocationContext context) { int offset = contentAssistRequest.getReplacementBeginPosition(); int length = contentAssistRequest.getReplacementLength(); Node node = contentAssistRequest.getNode(); // Node is 'parent' when the cursor is just before the end tag of the parent. Node parentNode = node.getNodeType() == Node.ELEMENT_NODE ? node : node.getParentNode(); if (parentNode.getNodeType() != Node.ELEMENT_NODE) return; Node typeAttr = null; String tagName = parentNode.getNodeName(); NamedNodeMap tagAttrs = parentNode.getAttributes(); if ("resultMap".equals(tagName)) typeAttr = tagAttrs.getNamedItem("type"); else if ("collection".equals(tagName)) typeAttr = tagAttrs.getNamedItem("ofType"); else if ("association".equals(tagName)) typeAttr = tagAttrs.getNamedItem("javaType"); if (typeAttr == null) return; String typeValue = typeAttr.getNodeValue(); if (typeValue == null || typeValue.length() == 0) return; IJavaProject project = getJavaProject(contentAssistRequest); // Try resolving the alias. String qualifiedName = TypeAliasCache.getInstance().resolveAlias(project, typeValue, null); if (qualifiedName == null) { // Assumed to be FQN. qualifiedName = typeValue; } BeanPropertyInfo beanProps = BeanPropertyCache.getBeanPropertyInfo(project, qualifiedName); try { Set<String> existingProps = new HashSet<String>(); NodeList existingPropNodes = XpathUtil.xpathNodes(parentNode, "*[@property]/@property"); for (int i = 0; i < existingPropNodes.getLength(); i++) { existingProps.add(existingPropNodes.item(i).getNodeValue()); } StringBuilder resultTags = new StringBuilder(); for (Entry<String, String> prop : beanProps.getWritableFields().entrySet()) { String propName = prop.getKey(); if (!existingProps.contains(propName)) { resultTags.append("<result property=\"").append(propName).append("\" column=\"") .append(propName).append("\" />\n"); } } contentAssistRequest.addProposal(new CompletionProposal(resultTags.toString(), offset, length, resultTags.length(), Activator.getIcon(), "<result /> for properties", new ContextInformation("column", "COLUMN"), null)); } catch (XPathExpressionException e) { Activator.log(Status.ERROR, e.getMessage(), e); } } protected void addAttributeValueProposals(ContentAssistRequest contentAssistRequest, CompletionProposalInvocationContext context) { IDOMNode node = (IDOMNode) contentAssistRequest.getNode(); String tagName = node.getNodeName(); 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; } // get the attribute in question (first attr name to the left of the cursor) String attributeName = null; if (nameRegion != null) attributeName = open.getText(nameRegion); ProposalType proposalType = resolveProposalType(tagName, attributeName); if (ProposalType.None.equals(proposalType)) { return; } IJavaProject project = getJavaProject(contentAssistRequest); String currentValue = null; if (contentAssistRequest.getRegion().getType() == DOMRegionContext.XML_TAG_ATTRIBUTE_VALUE) currentValue = contentAssistRequest.getText(); else currentValue = ""; String matchString = null; int matchStrLen = contentAssistRequest.getMatchString().length(); int start = contentAssistRequest.getReplacementBeginPosition(); int length = contentAssistRequest.getReplacementLength(); if (currentValue.length() > StringUtils.strip(currentValue).length() && (currentValue.startsWith("\"") || currentValue.startsWith("'")) && matchStrLen > 0) { // Value is surrounded by (double) quotes. matchString = currentValue.substring(1, matchStrLen); start++; length = currentValue.length() - 2; } else { matchString = currentValue.substring(0, matchStrLen); } try { switch (proposalType) { case Package: proposePackage(contentAssistRequest, project, matchString, start, length); break; case TypeAlias: proposeJavaType(contentAssistRequest, project, matchString, start, length, false); break; case ResultType: proposeJavaType(contentAssistRequest, project, matchString, start, length, true); break; case ResultProperty: proposeProperty(contentAssistRequest, matchString, start, length, node); break; case TypeHandlerType: proposeTypeHandler(contentAssistRequest, project, matchString, start, length); break; case CacheType: proposeCacheType(contentAssistRequest, project, matchString, start, length); break; case StatementId: proposeStatementId(contentAssistRequest, project, matchString, start, length, node); break; case MapperNamespace: proposeMapperNamespace(contentAssistRequest, project, start, length); break; case ResultMap: // TODO: Exclude the current resultMap id when proposing 'extends' proposeResultMapReference(contentAssistRequest, project, node, currentValue, matchString, matchStrLen, start, length); break; case Include: proposeReference(contentAssistRequest, project, node, matchString, start, length, "sql", null); break; case SelectId: proposeReference(contentAssistRequest, project, node, matchString, start, length, "select", null); break; default: break; } } catch (Exception e) { Activator.log(Status.ERROR, e.getMessage(), e); } } private void proposeResultMapReference(ContentAssistRequest contentAssistRequest, IJavaProject project, IDOMNode node, String currentValue, String matchString, int matchStrLen, int start, int length) throws XPathExpressionException, IOException, CoreException { String latterMaps = null; if (currentValue.indexOf(',') > -1) { int leftComma = matchString.lastIndexOf(','); int rightComma = currentValue.indexOf(',', matchStrLen); if (rightComma > -1) { latterMaps = currentValue.substring(rightComma, currentValue.length() - (currentValue.endsWith("\"") || currentValue.endsWith("'") ? 1 : 0)); } if (leftComma > -1) { start += leftComma + 1; matchString = matchString.substring(leftComma + 1).trim(); length -= leftComma + 1; } } proposeReference(contentAssistRequest, project, node, matchString, start, length, "resultMap", latterMaps); } private void proposeReference(ContentAssistRequest contentAssistRequest, IJavaProject project, IDOMNode node, String matchString, int start, int length, String targetElement, String replacementSuffix) throws XPathExpressionException, IOException, CoreException { int lastDot = matchString.lastIndexOf('.'); if (lastDot == -1) { char[] matchChrs = matchString.toCharArray(); NodeList nodes = XpathUtil.xpathNodes(node.getOwnerDocument(), "//" + targetElement + "/@id"); proposalFromNodes(contentAssistRequest, nodes, null, matchChrs, start, length, replacementSuffix); proposeNamespace(contentAssistRequest, project, node, "", matchChrs, start, length, replacementSuffix); } else { String namespace = matchString.substring(0, lastDot); char[] matchChrs = matchString.substring(lastDot + 1).toCharArray(); IFile mapperFile = MapperNamespaceCache.getInstance().get(project, namespace, null); IStructuredModel model = null; try { if (mapperFile != null) { model = StructuredModelManager.getModelManager().getModelForRead(mapperFile); IDOMModel domModel = (IDOMModel) model; IDOMDocument mapperDoc = domModel.getDocument(); NodeList nodes = XpathUtil.xpathNodes(mapperDoc, "//" + targetElement + "/@id"); proposalFromNodes(contentAssistRequest, nodes, namespace, matchChrs, start, length, replacementSuffix); } proposeNamespace(contentAssistRequest, project, node, namespace, matchChrs, start, length, replacementSuffix); } finally { if (model != null) { model.releaseFromRead(); } } } } private void proposalFromNodes(ContentAssistRequest contentAssistRequest, NodeList nodes, String namespace, char[] matchChrs, int start, int length, String replacementSuffix) { for (int j = 0; j < nodes.getLength(); j++) { String id = nodes.item(j).getNodeValue(); if (matchChrs.length == 0 || CharOperation.camelCaseMatch(matchChrs, id.toCharArray())) { StringBuilder replacementStr = new StringBuilder(); if (namespace != null && namespace.length() > 0) replacementStr.append(namespace).append('.'); replacementStr.append(id); int cursorPos = replacementStr.length(); if (replacementSuffix != null) replacementStr.append(replacementSuffix); ICompletionProposal proposal = new CompletionProposal(replacementStr.toString(), start, length, cursorPos, Activator.getIcon(), id, null, null); contentAssistRequest.addProposal(proposal); } } } private void proposeNamespace(ContentAssistRequest contentAssistRequest, IJavaProject project, IDOMNode node, String partialNamespace, char[] matchChrs, int start, int length, String replacementSuffix) throws XPathExpressionException { String currentNamespace = XpathUtil.xpathString(node.getOwnerDocument(), "//mapper/@namespace"); final List<ICompletionProposal> results = new ArrayList<ICompletionProposal>(); for (String namespace : MapperNamespaceCache.getInstance().getCacheMap(project, null).keySet()) { if (!namespace.equals(currentNamespace) && namespace.startsWith(partialNamespace) && !namespace.equals(partialNamespace)) { char[] simpleName = CharOperation.lastSegment(namespace.toCharArray(), '.'); if (matchChrs.length == 0 || CharOperation.camelCaseMatch(matchChrs, simpleName)) { StringBuilder replacementStr = new StringBuilder().append(namespace).append('.'); int cursorPos = replacementStr.length(); if (replacementSuffix != null) replacementStr.append(replacementSuffix); String displayString = new StringBuilder().append(simpleName).append(" - ").append(namespace) .toString(); results.add(new CompletionProposal(replacementStr.toString(), start, length, cursorPos, Activator.getIcon("/icons/mybatis-ns.png"), displayString, null, null)); } } } addProposals(contentAssistRequest, results); } private void proposeMapperNamespace(ContentAssistRequest contentAssistRequest, IJavaProject project, int start, int length) { // Calculate namespace from the file's classpath. String namespace = MybatipseXmlUtil.getJavaMapperType(project); ICompletionProposal proposal = new CompletionProposal(namespace, start, length, namespace.length(), Activator.getIcon("/icons/mybatis-ns.png"), namespace, null, null); contentAssistRequest.addProposal(proposal); } private void proposeStatementId(ContentAssistRequest contentAssistRequest, IJavaProject project, String matchString, int start, int length, IDOMNode node) throws JavaModelException, XPathExpressionException { final List<ICompletionProposal> results = new ArrayList<ICompletionProposal>(); String qualifiedName = MybatipseXmlUtil.getNamespace(node.getOwnerDocument()); IType type = project.findType(qualifiedName); for (IMethod method : type.getMethods()) { String statementId = method.getElementName(); if (matchString.length() == 0 || CharOperation.camelCaseMatch(matchString.toCharArray(), statementId.toCharArray())) { results.add(new CompletionProposal(statementId, start, length, statementId.length(), Activator.getIcon(), statementId, null, null)); } } addProposals(contentAssistRequest, results); } private void proposeProperty(ContentAssistRequest contentAssistRequest, String matchString, int start, int length, IDOMNode node) throws JavaModelException { String javaType = MybatipseXmlUtil.findEnclosingType(node); if (javaType != null && !MybatipseXmlUtil.isDefaultTypeAlias(javaType)) { IJavaProject project = getJavaProject(contentAssistRequest); IType type = project.findType(javaType); if (type == null) { javaType = TypeAliasCache.getInstance().resolveAlias(project, javaType, null); if (javaType == null) return; } Map<String, String> fields = BeanPropertyCache.searchFields(project, javaType, matchString, false, -1, false); List<ICompletionProposal> proposals = BeanPropertyCache.buildFieldNameProposal(fields, matchString, start, length); for (ICompletionProposal proposal : proposals) { contentAssistRequest.addProposal(proposal); } } } private void proposePackage(final ContentAssistRequest contentAssistRequest, IJavaProject project, String matchString, final int start, final int length) throws CoreException { final List<ICompletionProposal> results = new ArrayList<ICompletionProposal>(); final Set<String> foundPkgs = new HashSet<String>(); int includeMask = IJavaSearchScope.SOURCES | IJavaSearchScope.REFERENCED_PROJECTS; // Include application libraries only when package is specified (for better performance). boolean pkgSpecified = matchString != null && matchString.indexOf('.') > 0; if (pkgSpecified) includeMask |= IJavaSearchScope.APPLICATION_LIBRARIES | IJavaSearchScope.SYSTEM_LIBRARIES; IJavaSearchScope scope = SearchEngine.createJavaSearchScope(new IJavaProject[] { project }, includeMask); SearchRequestor requestor = new SearchRequestor() { @Override public void acceptSearchMatch(SearchMatch match) throws CoreException { PackageFragment element = (PackageFragment) match.getElement(); String pkg = element.getElementName(); if (pkg != null && pkg.length() > 0 && !foundPkgs.contains(pkg)) { foundPkgs.add(pkg); results.add(new CompletionProposal(pkg, start, length, pkg.length(), Activator.getIcon(), pkg, null, null)); } } }; searchPackage(matchString, scope, requestor); addProposals(contentAssistRequest, results); } private void searchPackage(String matchString, IJavaSearchScope scope, SearchRequestor requestor) throws CoreException { SearchPattern pattern = SearchPattern.createPattern(matchString + "*", IJavaSearchConstants.PACKAGE, IJavaSearchConstants.DECLARATIONS, SearchPattern.R_PREFIX_MATCH); SearchEngine searchEngine = new SearchEngine(); searchEngine.search(pattern, new SearchParticipant[] { SearchEngine.getDefaultSearchParticipant() }, scope, requestor, null); } private void proposeCacheType(final ContentAssistRequest contentAssistRequest, IJavaProject project, String matchString, final int start, final int length) throws JavaModelException { final List<ICompletionProposal> results = new ArrayList<ICompletionProposal>(); IType cache; IJavaSearchScope scope; cache = project.findType("org.apache.ibatis.cache.Cache"); if (cache == null) return; scope = SearchEngine.createHierarchyScope(cache); TypeNameRequestor requestor = new JavaTypeNameRequestor() { @Override public void acceptType(int modifiers, char[] packageName, char[] simpleTypeName, char[][] enclosingTypeNames, String path) { // Ignore abstract classes. if (Flags.isAbstract(modifiers)) return; addJavaTypeProposal(results, start, length, packageName, simpleTypeName, enclosingTypeNames); } }; searchJavaType(matchString, scope, requestor); addProposals(contentAssistRequest, results); } private void proposeTypeHandler(final ContentAssistRequest contentAssistRequest, IJavaProject project, String matchString, final int start, final int length) throws JavaModelException { final List<ICompletionProposal> results = new ArrayList<ICompletionProposal>(); IType typeHandler; IJavaSearchScope scope; typeHandler = project.findType("org.apache.ibatis.type.TypeHandler"); if (typeHandler == null) return; scope = SearchEngine.createHierarchyScope(typeHandler); TypeNameRequestor requestor = new JavaTypeNameRequestor() { @Override public void acceptType(int modifiers, char[] packageName, char[] simpleTypeName, char[][] enclosingTypeNames, String path) { // Ignore abstract classes. if (Flags.isAbstract(modifiers)) return; addJavaTypeProposal(results, start, length, packageName, simpleTypeName, enclosingTypeNames); } }; searchJavaType(matchString, scope, requestor); addProposals(contentAssistRequest, results); } private void proposeJavaType(final ContentAssistRequest contentAssistRequest, IJavaProject project, String matchString, final int start, final int length, boolean includeAlias) throws JavaModelException { final List<ICompletionProposal> results = new ArrayList<ICompletionProposal>(); if (includeAlias) { TypeAliasMap typeAliasMap = TypeAliasCache.getInstance().getTypeAliasMap(project, null); for (TypeAliasInfo typeAliasInfo : typeAliasMap.values()) { String alias = typeAliasInfo.getAliasToInsert(); String qualifiedName = typeAliasInfo.getQualifiedName(); char[] aliasChrs = alias.toCharArray(); char[] matchChrs = matchString.toCharArray(); if (matchString.length() == 0 || CharOperation.camelCaseMatch(matchChrs, aliasChrs) || CharOperation.prefixEquals(matchChrs, aliasChrs, false)) { results.add(new CompletionProposal(alias, start, length, alias.length(), Activator.getIcon("/icons/mybatis-alias.png"), alias + " - " + qualifiedName, null, null)); } } addProposals(contentAssistRequest, results); } results.clear(); int includeMask = IJavaSearchScope.SOURCES | IJavaSearchScope.REFERENCED_PROJECTS; // Include application libraries only when package is specified (for better performance). boolean pkgSpecified = matchString != null && matchString.indexOf('.') > 0; if (pkgSpecified) includeMask |= IJavaSearchScope.APPLICATION_LIBRARIES; IJavaSearchScope scope = SearchEngine.createJavaSearchScope(new IJavaProject[] { project }, includeMask); TypeNameRequestor requestor = new JavaTypeNameRequestor() { @Override public void acceptType(int modifiers, char[] packageName, char[] simpleTypeName, char[][] enclosingTypeNames, String path) { if (Flags.isAbstract(modifiers) || Flags.isInterface(modifiers)) return; addJavaTypeProposal(results, start, length, packageName, simpleTypeName, enclosingTypeNames); } }; searchJavaType(matchString, scope, requestor); addProposals(contentAssistRequest, results); } private void searchJavaType(String matchString, IJavaSearchScope scope, TypeNameRequestor requestor) throws JavaModelException { char[] searchPkg = null; char[] searchType = null; if (matchString != null && matchString.length() > 0) { char[] match = matchString.toCharArray(); int lastDotPos = matchString.lastIndexOf('.'); if (lastDotPos == -1) { searchType = match; } else { if (lastDotPos + 1 < match.length) { searchType = CharOperation.lastSegment(match, '.'); } searchPkg = Arrays.copyOfRange(match, 0, lastDotPos); } } SearchEngine searchEngine = new SearchEngine(); searchEngine.searchAllTypeNames(searchPkg, SearchPattern.R_PREFIX_MATCH, searchType, SearchPattern.R_CAMELCASE_MATCH, IJavaSearchConstants.CLASS, scope, requestor, IJavaSearchConstants.WAIT_UNTIL_READY_TO_SEARCH, null); } private void addProposals(final ContentAssistRequest contentAssistRequest, List<ICompletionProposal> proposals) { Collections.sort(proposals, new CompletionProposalComparator()); for (ICompletionProposal proposal : proposals) { contentAssistRequest.addProposal(proposal); } } private ProposalType resolveProposalType(String tag, String attr) { // TODO: proxyFactory, logImpl if ("mapper".equals(tag) && "namespace".equals(attr)) return ProposalType.MapperNamespace; else if ("type".equals(attr) && "typeAlias".equals(tag)) return ProposalType.TypeAlias; else if ("type".equals(attr) && "cache".equals(tag)) return ProposalType.CacheType; else if ("type".equals(attr) && "objectFactory".equals(tag)) return ProposalType.None; // TODO propose object factory else if ("type".equals(attr) && "objectWrapperFactory".equals(tag)) return ProposalType.None; // TODO propose object wrapper factory else if ("type".equals(attr) || "resultType".equals(attr) || "parameterType".equals(attr) || "ofType".equals(attr) || "javaType".equals(attr)) return ProposalType.ResultType; else if ("property".equals(attr)) return ProposalType.ResultProperty; else if ("package".equals(tag) && "name".equals(attr)) return ProposalType.Package; else if ("keyProperty".equals(attr)) return ProposalType.None; // TODO propose key property? else if ("typeHandler".equals(attr) || "handler".equals(attr)) return ProposalType.TypeHandlerType; else if ("resultMap".equals(attr) || "extends".equals(attr)) return ProposalType.ResultMap; else if ("refid".equals(attr)) return ProposalType.Include; else if ("select".equals(attr)) return ProposalType.SelectId; else if ("id".equals(attr) && ("select".equals(tag) || "update".equals(tag) || "insert".equals(tag) || "delete".equals(tag))) return ProposalType.StatementId; return ProposalType.None; } abstract class JavaTypeNameRequestor extends TypeNameRequestor { protected void addJavaTypeProposal(final List<ICompletionProposal> results, final int start, final int length, char[] packageName, char[] simpleTypeName, char[][] enclosingTypeNames) { String typeFqn = NameUtil.buildQualifiedName(packageName, simpleTypeName, enclosingTypeNames, true); String displayStr = new StringBuilder().append(simpleTypeName).append(" - ").append(packageName) .toString(); results.add(new CompletionProposal(typeFqn, start, length, typeFqn.length(), Activator.getIcon(), displayStr, null, null)); } } private IJavaProject getJavaProject(ContentAssistRequest request) { if (request != null) { IStructuredDocumentRegion region = request.getDocumentRegion(); if (region != null) { IDocument document = region.getParentDocument(); return MybatipseXmlUtil.getJavaProject(document); } } return null; } private class CompletionProposalComparator implements Comparator<ICompletionProposal> { @Override public int compare(ICompletionProposal p1, ICompletionProposal p2) { return p1.getDisplayString().compareToIgnoreCase(p2.getDisplayString()); } } }