/******************************************************************************* * Copyright (c) 2009 Abstratt Technologies * 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: * Attila Bak - initial API and implementation *******************************************************************************/ package com.abstratt.mdd.internal.ui; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.eclipse.core.resources.IProject; import org.eclipse.core.runtime.CoreException; import org.eclipse.emf.common.util.EList; import org.eclipse.emf.common.util.TreeIterator; import org.eclipse.emf.ecore.EClass; import org.eclipse.emf.ecore.EObject; import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.IRegion; import org.eclipse.jface.text.ITextViewer; import org.eclipse.jface.text.contentassist.CompletionProposal; import org.eclipse.jface.text.contentassist.ICompletionProposal; import org.eclipse.jface.text.contentassist.IContentAssistProcessor; import org.eclipse.jface.text.contentassist.IContextInformation; import org.eclipse.jface.text.contentassist.IContextInformationValidator; import org.eclipse.uml2.uml.Class; import org.eclipse.uml2.uml.Element; import org.eclipse.uml2.uml.Enumeration; import org.eclipse.uml2.uml.NamedElement; import org.eclipse.uml2.uml.Package; import org.eclipse.uml2.uml.PrimitiveType; import org.eclipse.uml2.uml.Profile; import org.eclipse.uml2.uml.Property; import org.eclipse.uml2.uml.Stereotype; import org.eclipse.uml2.uml.UMLPackage.Literals; import com.abstratt.mdd.core.IRepository; import com.abstratt.mdd.internal.ui.editors.source.SourceEditor; import com.abstratt.mdd.ui.UIUtils; /** * Content assist processor for TextUML. * * FIXME This implementation has a few caveats that we need to address: - it * allows access to all elements in the same repository - it ignores profile * applications - all stereotypes in any profiles in the repository are offered * - ... */ public class TextUMLCompletionProcessor implements IContentAssistProcessor { private static final String EMPTY_STRING = ""; private static final String UML = "uml"; private static final String TEXTUML_DEPENDENCY = "dependency"; private static final String TEXTUML_CLASS = "class"; private static final String TEXTUML_INTERFACE = "interface"; private static final String TEXTUML_ASSOCIATION = "association"; private static final String TEXTUML_OPERATION = "operation"; private static final String TEXTUML_ROLE = "role"; private static final String TEXTUML_ATTRIBUTE = "attribute"; private static final String TEXTUML_CONSTANT = "constant"; private static final String TEXTUML_DATATYPE = "datatype"; private static final String TEXTUML_PRIMITIVE = "primitive"; // maps TextUML keywords to the corresponding Ecore classes private static final Object[][] TARGET_METACLASSES = { { TEXTUML_DEPENDENCY, Literals.DEPENDENCY }, { TEXTUML_CLASS, Literals.CLASS }, { TEXTUML_ASSOCIATION, Literals.ASSOCIATION }, { TEXTUML_INTERFACE, Literals.INTERFACE }, { TEXTUML_PRIMITIVE, Literals.PRIMITIVE_TYPE }, { TEXTUML_DATATYPE, Literals.DATA_TYPE }, { TEXTUML_OPERATION, Literals.OPERATION }, { TEXTUML_ROLE, Literals.PROPERTY }, { TEXTUML_ATTRIBUTE, Literals.PROPERTY }, { TEXTUML_CONSTANT, Literals.PROPERTY } }; private static final String BASE_PROPERTY = "base_"; private static final String ECORE = "Ecore"; private static final String STANDARD = "Standard"; private SourceEditor editor = null; private String patternStr = "([a-zA-Z0-9])+[:][:]([a-zA-Z0-9])+"; private Pattern pattern = null; protected final static String[] defaultProposals = com.abstratt.mdd.frontend.textuml.core.TextUMLConstants.KEYWORDS; public TextUMLCompletionProcessor(SourceEditor editor) { super(); this.editor = editor; pattern = Pattern.compile(patternStr); } /* * (non-Javadoc) * * @see org.eclipse.jface.text.contentassist.IContentAssistProcessor# * computeCompletionProposals(org.eclipse.jface.text.ITextViewer, int) */ public ICompletionProposal[] computeCompletionProposals(ITextViewer viewer, int offset) { List<ICompletionProposal> finalProposals = new ArrayList<ICompletionProposal>(); List<String> proposals = new ArrayList<String>(); ICompletionProposal[] toReturn = null; try { toReturn = doComputeProposal(viewer, offset, finalProposals, proposals); } catch (Exception ex) { toReturn = new ICompletionProposal[proposals.size()]; return proposals.toArray(toReturn); } return toReturn; } private ICompletionProposal[] doComputeProposal(ITextViewer viewer, int offset, List<ICompletionProposal> finalProposals, List<String> proposals) { IRepository repository = getRepository(); // Safety check if (repository == null) { ICompletionProposal[] dummyRet = new ICompletionProposal[proposals.size()]; return proposals.toArray(dummyRet); } IDocument doc = viewer.getDocument(); int lineNumber = -1; try { lineNumber = doc.getLineOfOffset(offset); } catch (BadLocationException e) { lineNumber = -1; } // Safety check if (lineNumber == -1) { ICompletionProposal[] dummyRet = new ICompletionProposal[proposals.size()]; return proposals.toArray(dummyRet); } // FIXME this relies on specific formatting. Should use new API in // TextUMLCompiler (and enhance it if necessary) // in order to find out the context // Getting the current and the next line String currentLine = getLineText(viewer, lineNumber); String nextLine = getLineText(viewer, lineNumber + 1); // Getting the last word tipped for the code completion to enable // incremental search (only for keywords) String lastWord = lastWord(doc, offset); // If we are in an attribute, operation or role we look up the types // that are available and return if (currentLine.indexOf(TEXTUML_ATTRIBUTE) > -1 || currentLine.indexOf(TEXTUML_ROLE) > -1 || currentLine.indexOf(TEXTUML_OPERATION) > -1 || currentLine.indexOf(TEXTUML_DEPENDENCY) > -1) { findTypes(proposals, lastWord); for (Iterator<String> iterator = proposals.iterator(); iterator.hasNext();) { String string = iterator.next(); if (checkifNotEmpty(lastWord)) { finalProposals.add(new CompletionProposal(string, offset - lastWord.length(), lastWord.length(), string.length())); } else { finalProposals.add(new CompletionProposal(string, offset, 0, string.length())); } } return createProposalList(finalProposals); } // If we are on an "empty" line or before the end in somewhere we // deliver the default keyword proposals if (nextLine.trim().length() == 0 || nextLine.indexOf("end") > -1) { for (int i = 0; i < defaultProposals.length; i++) { if (defaultProposals[i].startsWith(lastWord)) { finalProposals.add(new CompletionProposal(defaultProposals[i], offset - lastWord.length(), lastWord .length(), defaultProposals[i].length())); } } return createProposalList(finalProposals); } int currentLineOffset = -1; String currentLineToInspect = EMPTY_STRING; try { currentLineOffset = doc.getLineOffset(lineNumber); currentLineToInspect = doc.get(currentLineOffset, offset - currentLineOffset); } catch (BadLocationException e) { currentLineToInspect = EMPTY_STRING; } Matcher matcher = pattern.matcher(currentLineToInspect); boolean matchFound = false; int lastEnd = 0; String profileWithStereotype = EMPTY_STRING; // Try to get the stereotype where we are currently working on the // current line while (matcher.find()) { matchFound = true; profileWithStereotype = matcher.group(); lastEnd = matcher.end(); } if (matchFound) { // We have found stereotypes already applied // Get the remaining of the line where we are currently String remaining = currentLineToInspect.substring(lastEnd); if (remaining.indexOf("(") > -1) { // We might be working in the tagged values section if (remaining.indexOf(")") > -1 && remaining.indexOf(",") > -1 && remaining.indexOf(",") > remaining.indexOf(")")) { // In this case we are outside of the tagged values section // because it look like // Stereotype(taggedvaluename="something"), findStereotypesForNextLine(proposals, nextLine); removeUnneededBecauseOfCurrentLine(proposals, currentLine); } else { // We are in a tagged value section for sure the pattern is: // Stereotype(taggedvaluename String[] stringArr = profileWithStereotype.substring(0, profileWithStereotype.length()).split("::"); findTaggedValuesForCurrentLineButNotApplied(proposals, stringArr[0], stringArr[1], currentLine); } } else { // Normal stereotype application we are not editing tagged // values findStereotypesForNextLine(proposals, nextLine); removeUnneededBecauseOfCurrentLine(proposals, currentLine); } } else { // We have nothing applied on the current line try to get some // values for the next line findStereotypesForNextLine(proposals, nextLine); } for (Iterator<String> iterator = proposals.iterator(); iterator.hasNext();) { String string = iterator.next(); finalProposals.add(new CompletionProposal(string, offset, 0, string.length())); } return createProposalList(finalProposals); } private ICompletionProposal[] createProposalList(List<ICompletionProposal> finalProposals) { ICompletionProposal[] a = new ICompletionProposal[finalProposals.size()]; return finalProposals.toArray(a); } private boolean checkifNotEmpty(String word) { return word != null && !word.equals(EMPTY_STRING); } private void removeUnneededBecauseOfCurrentLine(List<String> proposals, String currentLine) { List<String> result = new ArrayList<String>(); for (Iterator<String> iterator = proposals.iterator(); iterator.hasNext();) { String string = iterator.next(); String replaced = string.replaceAll("\\[", EMPTY_STRING).replaceAll("\\]", EMPTY_STRING); if (currentLine.indexOf(replaced) == -1) { result.add(replaced); } } proposals.clear(); proposals.addAll(result); } private void findTypes(List<String> proposals, String lastWord) { Boolean lastWordEmpty = true; if (checkifNotEmpty(lastWord)) { lastWord = lastWord.toLowerCase(); lastWordEmpty = false; } Package[] packages = getValidPackages(); for (int i = 0; i < packages.length; i++) { for (TreeIterator<EObject> iterator = packages[i].eAllContents(); iterator.hasNext();) { EObject object = iterator.next(); if (object instanceof Class) { Class clazz = (Class) object; if (lastWordEmpty || clazz.getQualifiedName().toLowerCase().contains(lastWord)) { proposals.add(clazz.getQualifiedName()); } } if (object instanceof PrimitiveType) { PrimitiveType primo = (PrimitiveType) object; if (lastWordEmpty || primo.getQualifiedName().toLowerCase().contains(lastWord)) { proposals.add(primo.getQualifiedName()); } } if (object instanceof Enumeration) { Enumeration enume = (Enumeration) object; if (lastWordEmpty || enume.getQualifiedName().toLowerCase().contains(lastWord)) { proposals.add(enume.getQualifiedName()); } } } } } private void findTaggedValuesForCurrentLineButNotApplied(List<String> proposals, String profileName, String stereotypeName, String currentLine) { List<String> result = findTaggedAttributesForStereotype(findStereotypeByName(profileName, stereotypeName)); for (Iterator<String> iterator = result.iterator(); iterator.hasNext();) { String string = iterator.next(); if (string.indexOf(BASE_PROPERTY) == -1 && currentLine.indexOf(string) == -1) { proposals.add(string + "=\"\""); } } } private IRepository getRepository() { IProject currentProject = editor.getWorkingCopy().getFile().getProject(); try { return UIUtils.getCachedRepository(currentProject); } catch (CoreException e) { } return null; } private void findStereotypesForNextLine(List<String> proposals, String nextLine) { NamedElement target = null; for (Object[] targets : TARGET_METACLASSES) { final String targetKeyword = (String) targets[0]; if (nextLine.indexOf(targetKeyword) >= 0) { final EClass targetMetaclass = (EClass) targets[1]; selectProposalsFromStereotypes(proposals, findStereotypesForExtendedClass(getStereotypes(), targetMetaclass)); return; } } } private void selectProposalsFromStereotypes(List<String> proposals, List<Stereotype> stereos) { for (Iterator<Stereotype> iterator = stereos.iterator(); iterator.hasNext();) { Stereotype stereotype = iterator.next(); proposals.add("[" + stereotype.getProfile().getName() + "::" + stereotype.getName() + "]"); } } private List<String> findTaggedAttributesForStereotype(Stereotype stereotype) { List<String> result = new ArrayList<String>(); for (Iterator<Property> iterator = stereotype.getAllAttributes().iterator(); iterator.hasNext();) { Property property = iterator.next(); result.add(property.getName()); } return result; } private Stereotype findStereotypeByName(String profileName, String name) { Profile profile = findProfileByName(profileName); if (profile == null) return null; EList<Element> elements = profile.getOwnedElements(); for (Iterator<Element> iterator = elements.iterator(); iterator.hasNext();) { Element element = iterator.next(); if (element instanceof Stereotype) if (((Stereotype) element).getName().equals(name)) return (Stereotype) element; } return null; } private String lastWord(IDocument doc, int offset) { String result = EMPTY_STRING; try { for (int n = offset - 1; n >= 0; n--) { char c = doc.getChar(n); if (!Character.isJavaIdentifierPart(c)) return doc.get(n + 1, offset - n - 1); } } catch (BadLocationException e) { result = EMPTY_STRING; } return result; } private Profile findProfileByName(String name) { Package[] packages = getRepository().getTopLevelPackages(null); Profile[] validProfiles = getValidProfiles(packages); for (int i = 0; i < validProfiles.length; i++) if (validProfiles[i].getName().equals(name)) return validProfiles[i]; return null; } private List<Stereotype> findStereotypesForExtendedClass(List<Stereotype> stereos, EClass targetMetaclass) { List<Stereotype> result = new ArrayList<Stereotype>(); for (Iterator<Stereotype> iterator = stereos.iterator(); iterator.hasNext();) { Stereotype stereotype = iterator.next(); for (Iterator<Class> clazzIter = stereotype.getAllExtendedMetaclasses().iterator(); clazzIter.hasNext();) { Class clazz = clazzIter.next(); // FIXME this does not handle inheritance (a stereotype for a // base metaclass should apply to any sub metaclasses) if (clazz.getName().equals(targetMetaclass.getName())) result.add(stereotype); } } return result; } private String getLineText(ITextViewer viewer, int lineNumber) { IDocument doc = viewer.getDocument(); String result; try { IRegion lineRegion = doc.getLineInformation(lineNumber); result = doc.get(lineRegion.getOffset(), lineRegion.getLength()); } catch (BadLocationException e) { result = EMPTY_STRING; } return result; } private Package[] getValidPackages() { Package[] packages = getRepository().getTopLevelPackages(null); List<Package> validPackages = new ArrayList<Package>(); for (int i = 0; i < packages.length; i++) { Package p = (Package) packages[i]; if (!(p instanceof Profile) && !p.getName().equals(STANDARD) && !p.getName().equals(UML) && !p.getName().equalsIgnoreCase(ECORE)) validPackages.add((Package) p); } return validPackages.toArray(new Package[validPackages.size()]); } private Profile[] getValidProfiles(Package[] packages) { List<Profile> validPackages = new ArrayList<Profile>(); for (int i = 0; i < packages.length; i++) { Package p = (Package) packages[i]; if (p instanceof Profile && !p.getName().equals(STANDARD) && !p.getName().equals(ECORE)) { validPackages.add((Profile) p); } } return validPackages.toArray(new Profile[validPackages.size()]); } private List<Stereotype> getStereotypes() { List<Stereotype> result = new ArrayList<Stereotype>(); Package[] packages = getRepository().getTopLevelPackages(null); Profile[] validProfiles = getValidProfiles(packages); for (int i = 0; i < validProfiles.length; i++) { EList<Element> elements = validProfiles[i].getOwnedElements(); for (Iterator<Element> iterator = elements.iterator(); iterator.hasNext();) { Element element = iterator.next(); if (element instanceof Stereotype) result.add((Stereotype) element); } } return result; } /* * (non-Javadoc) * * @see org.eclipse.jface.text.contentassist.IContentAssistProcessor# * computeContextInformation(org.eclipse.jface.text.ITextViewer, int) */ public IContextInformation[] computeContextInformation(ITextViewer viewer, int offset) { return null; } /* * (non-Javadoc) * * @see org.eclipse.jface.text.contentassist.IContentAssistProcessor# * getCompletionProposalAutoActivationCharacters() */ public char[] getCompletionProposalAutoActivationCharacters() { return new char[] { '[' }; } /* * (non-Javadoc) * * @see org.eclipse.jface.text.contentassist.IContentAssistProcessor# * getContextInformationAutoActivationCharacters() */ public char[] getContextInformationAutoActivationCharacters() { return null; } /* * (non-Javadoc) * * @see org.eclipse.jface.text.contentassist.IContentAssistProcessor# * getContextInformationValidator() */ public IContextInformationValidator getContextInformationValidator() { return null; } /* * (non-Javadoc) * * @see * org.eclipse.jface.text.contentassist.IContentAssistProcessor#getErrorMessage * () */ public String getErrorMessage() { return "Error in code completion"; } }