content: /*******************************************************************************
   * 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";
      }
  
  }
  