Bug 422960 - SDK Support for @ClassId

https://bugs.eclipse.org/bugs/show_bug.cgi?id=422960

Search for duplicate class ids. Error markers and marker resolution.

Change-Id: I384ac002ff9821fdf95a1be2cfe96f7d11131440
Reviewed-on: https://git.eclipse.org/r/26995
Tested-by: Hudson CI
Reviewed-by: Matthias Villiger <mvi@bsiag.com>
diff --git a/org.eclipse.scout.nls.sdk/src/org/eclipse/scout/nls/sdk/internal/ui/editor/NlsTableCursor.java b/org.eclipse.scout.nls.sdk/src/org/eclipse/scout/nls/sdk/internal/ui/editor/NlsTableCursor.java
index a1b4da6..53e4705 100644
--- a/org.eclipse.scout.nls.sdk/src/org/eclipse/scout/nls/sdk/internal/ui/editor/NlsTableCursor.java
+++ b/org.eclipse.scout.nls.sdk/src/org/eclipse/scout/nls/sdk/internal/ui/editor/NlsTableCursor.java
@@ -328,11 +328,10 @@
     }
     m_renaming = true;
     try {
-      INlsEntry row = (NlsEntry) m_cursor.getRow().getData();
+      INlsEntry row = (INlsEntry) m_cursor.getRow().getData();
       for (INlsTableCursorManangerListener listener : m_listeners) {
         listener.textChangend(row, m_cursor.getColumn(), m_editingText.getText());
       }
-      //m_cursor.getRow().setText(m_cursor.getColumn(), m_editingText.getText());
     }
     finally {
       disposeText();
diff --git a/org.eclipse.scout.nls.sdk/src/org/eclipse/scout/nls/sdk/model/workspace/project/AbstractNlsProject.java b/org.eclipse.scout.nls.sdk/src/org/eclipse/scout/nls/sdk/model/workspace/project/AbstractNlsProject.java
index 1f0e631..5397b12 100644
--- a/org.eclipse.scout.nls.sdk/src/org/eclipse/scout/nls/sdk/model/workspace/project/AbstractNlsProject.java
+++ b/org.eclipse.scout.nls.sdk/src/org/eclipse/scout/nls/sdk/model/workspace/project/AbstractNlsProject.java
@@ -234,7 +234,7 @@
       StringBuilder ret = new StringBuilder(baseText.length());
 
       // remove not allowed characters
-      baseText = baseText.replaceAll("[^a-zA-Z0-9_.\\-\\s]*", "").trim();
+      baseText = baseText.replaceAll("[^a-zA-Z0-9_\\.\\- ]*", "").trim();
 
       // camel case multiple words
       String[] split = baseText.split(" ");
diff --git a/org.eclipse.scout.sdk.rap/src/org/eclipse/scout/sdk/rap/var/RapTargetVariable.java b/org.eclipse.scout.sdk.rap/src/org/eclipse/scout/sdk/rap/var/RapTargetVariable.java
index ec22ada..97fde18 100644
--- a/org.eclipse.scout.sdk.rap/src/org/eclipse/scout/sdk/rap/var/RapTargetVariable.java
+++ b/org.eclipse.scout.sdk.rap/src/org/eclipse/scout/sdk/rap/var/RapTargetVariable.java
@@ -10,9 +10,8 @@
  ******************************************************************************/
 package org.eclipse.scout.sdk.rap.var;
 
-import java.util.HashSet;
+import java.util.List;
 import java.util.Queue;
-import java.util.Set;
 import java.util.concurrent.ConcurrentLinkedQueue;
 
 import org.eclipse.core.resources.IFile;
@@ -20,7 +19,7 @@
 import org.eclipse.core.resources.IResourceChangeEvent;
 import org.eclipse.core.resources.IResourceChangeListener;
 import org.eclipse.core.resources.IResourceDelta;
-import org.eclipse.core.resources.IResourceDeltaVisitor;
+import org.eclipse.core.resources.IResourceProxy;
 import org.eclipse.core.resources.ResourcesPlugin;
 import org.eclipse.core.runtime.CoreException;
 import org.eclipse.core.runtime.IProgressMonitor;
@@ -35,6 +34,7 @@
 import org.eclipse.scout.commons.EventListenerList;
 import org.eclipse.scout.commons.StringUtility;
 import org.eclipse.scout.sdk.rap.ScoutSdkRap;
+import org.eclipse.scout.sdk.util.resources.IResourceFilter;
 import org.eclipse.scout.sdk.util.resources.ResourceUtility;
 
 /**
@@ -204,27 +204,13 @@
       return RESOURCE_CHANGE_JOB_FAMILY.equals(family);
     }
 
-    private void collectTargetFiles(IResourceDelta d, final Set<IFile> res) throws CoreException {
-      if (d == null) {
-        return;
-      }
-
-      try {
-        d.accept(new IResourceDeltaVisitor() {
-          @Override
-          public boolean visit(IResourceDelta delta) throws CoreException {
-            IResource resource = delta.getResource();
-            if (ResourceUtility.exists(resource) && resource.getType() == IResource.FILE && resource.getName().toLowerCase().endsWith(".target")) {
-              res.add((IFile) resource);
-              return false;
-            }
-            return true;
-          }
-        });
-      }
-      catch (CoreException e) {
-        ScoutSdkRap.logError("Could not check for .target files that use the scout rap target variable.", e);
-      }
+    private List<IResource> collectTargetFiles(IResourceDelta d) throws CoreException {
+      return ResourceUtility.getAllResources(d, new IResourceFilter() {
+        @Override
+        public boolean accept(IResourceProxy resource) {
+          return resource.getType() == IResource.FILE && resource.getName().toLowerCase().endsWith(".target");
+        }
+      });
     }
 
     @Override
@@ -233,10 +219,10 @@
       while ((event = m_resourceChangeEventsToHandle.poll()) != null) {
         if (!StringUtility.hasText(getValue())) {
           try {
-            HashSet<IFile> collector = new HashSet<IFile>();
-            collectTargetFiles(event.getDelta(), collector);
-            if (!collector.isEmpty()) {
-              for (IFile f : collector) {
+            List<IResource> files = collectTargetFiles(event.getDelta());
+            if (!files.isEmpty()) {
+              for (IResource r : files) {
+                IFile f = (IFile) r;
                 try {
                   if (isScoutRapTargetVarPresent(f)) {
                     fireEmptyVariableInUse(f);
diff --git a/org.eclipse.scout.sdk.ui/META-INF/MANIFEST.MF b/org.eclipse.scout.sdk.ui/META-INF/MANIFEST.MF
index 422519e..e0d1695 100644
--- a/org.eclipse.scout.sdk.ui/META-INF/MANIFEST.MF
+++ b/org.eclipse.scout.sdk.ui/META-INF/MANIFEST.MF
@@ -29,6 +29,7 @@
  org.eclipse.scout.sdk.ui.action.library,
  org.eclipse.scout.sdk.ui.action.rename,
  org.eclipse.scout.sdk.ui.action.validation,
+ org.eclipse.scout.sdk.ui.classid,
  org.eclipse.scout.sdk.ui.dialog,
  org.eclipse.scout.sdk.ui.extensions,
  org.eclipse.scout.sdk.ui.extensions.bundle,
diff --git a/org.eclipse.scout.sdk.ui/plugin.xml b/org.eclipse.scout.sdk.ui/plugin.xml
index c096d0b..91e5820 100644
--- a/org.eclipse.scout.sdk.ui/plugin.xml
+++ b/org.eclipse.scout.sdk.ui/plugin.xml
@@ -1197,4 +1197,11 @@
             requiredSourceLevel="1.6">
       </quickAssistProcessor>
    </extension>
+   <extension
+         point="org.eclipse.ui.ide.markerResolution">
+      <markerResolutionGenerator
+            class="org.eclipse.scout.sdk.ui.classid.ClassIdDuplicateResolutionGenerator"
+            markerType="org.eclipse.scout.sdk.classid.duplicate">
+      </markerResolutionGenerator>
+   </extension>
 </plugin>
diff --git a/org.eclipse.scout.sdk.ui/src/org/eclipse/scout/sdk/ui/classid/ClassIdDuplicateResolution.java b/org.eclipse.scout.sdk.ui/src/org/eclipse/scout/sdk/ui/classid/ClassIdDuplicateResolution.java
new file mode 100644
index 0000000..513e115
--- /dev/null
+++ b/org.eclipse.scout.sdk.ui/src/org/eclipse/scout/sdk/ui/classid/ClassIdDuplicateResolution.java
@@ -0,0 +1,137 @@
+/*******************************************************************************
+ * Copyright (c) 2013 BSI Business Systems Integration AG.
+ * 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:
+ *     BSI Business Systems Integration AG - initial API and implementation
+ ******************************************************************************/
+package org.eclipse.scout.sdk.ui.classid;
+
+import java.util.LinkedList;
+import java.util.List;
+
+import org.eclipse.core.resources.IMarker;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.jobs.IJobChangeEvent;
+import org.eclipse.core.runtime.jobs.JobChangeAdapter;
+import org.eclipse.jdt.core.IAnnotation;
+import org.eclipse.jdt.core.IJavaElement;
+import org.eclipse.jdt.core.IType;
+import org.eclipse.jdt.ui.JavaUI;
+import org.eclipse.scout.nls.sdk.model.INlsEntry;
+import org.eclipse.scout.sdk.Texts;
+import org.eclipse.scout.sdk.classid.ClassIdValidationJob;
+import org.eclipse.scout.sdk.extensions.classidgenerators.ClassIdGenerationContext;
+import org.eclipse.scout.sdk.extensions.classidgenerators.ClassIdGenerators;
+import org.eclipse.scout.sdk.jobs.OperationJob;
+import org.eclipse.scout.sdk.operation.IOperation;
+import org.eclipse.scout.sdk.operation.jdt.annotation.AnnotationNewOperation;
+import org.eclipse.scout.sdk.sourcebuilder.annotation.AnnotationSourceBuilderFactory;
+import org.eclipse.scout.sdk.ui.extensions.quickassist.ClassIdDocumentationSupport;
+import org.eclipse.scout.sdk.ui.internal.ScoutSdkUi;
+import org.eclipse.scout.sdk.util.type.TypeUtility;
+import org.eclipse.scout.sdk.util.typecache.IWorkingCopyManager;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.widgets.MessageBox;
+import org.eclipse.ui.IMarkerResolution;
+
+/**
+ * <h3>{@link ClassIdDuplicateResolution}</h3>
+ * 
+ * @author Matthias Villiger
+ * @since 4.0.0 21.05.2014
+ */
+public class ClassIdDuplicateResolution implements IMarkerResolution {
+
+  private final IAnnotation m_annotation;
+
+  public ClassIdDuplicateResolution(IAnnotation annotation) {
+    m_annotation = annotation;
+  }
+
+  @Override
+  public String getLabel() {
+    return Texts.get("UpdateWithNewClassIdValue");
+  }
+
+  @Override
+  public void run(final IMarker marker) {
+    final IType parent = (IType) m_annotation.getAncestor(IJavaElement.TYPE);
+    if (TypeUtility.exists(parent)) {
+      ClassIdDocumentationSupport support = new ClassIdDocumentationSupport(parent);
+      INlsEntry nlsEntry = support.getNlsEntry();
+      boolean migrateDocumentation = false;
+      boolean doMigration = true;
+      if (nlsEntry != null) {
+        // the id has documentation assigned
+        // either the doc is correct and belongs to this class -> must be migrated too
+        // or the doc belongs to another class with this id -> ignore
+        MessageBox msgbox = new MessageBox(ScoutSdkUi.getShell(), SWT.ICON_QUESTION | SWT.YES | SWT.NO | SWT.CANCEL);
+        msgbox.setText(Texts.get("KeepAssignedDocumentation"));
+        msgbox.setMessage(Texts.get("ThisClassIdHasADocumentationEntryAssigned"));
+        int answer = msgbox.open();
+        doMigration = answer != SWT.CANCEL;
+        migrateDocumentation = answer == SWT.YES;
+      }
+
+      List<IOperation> ops = new LinkedList<IOperation>();
+      if (doMigration) {
+        String newId = ClassIdGenerators.generateNewId(new ClassIdGenerationContext(parent));
+        ops.add(createUpdateAnnotationInJavaSourceOperation(parent, newId));
+        if (migrateDocumentation) {
+          ops.add(createUpdateClassIdInDocumentationOperation(nlsEntry, newId));
+        }
+      }
+
+      if (!ops.isEmpty()) {
+        OperationJob j = new OperationJob(ops);
+        j.addJobChangeListener(new JobChangeAdapter() {
+          @Override
+          public void done(IJobChangeEvent event) {
+            try {
+              marker.delete();
+            }
+            catch (CoreException e) {
+              //nop
+            }
+            try {
+              JavaUI.openInEditor(parent);
+            }
+            catch (Exception e) {
+              e.printStackTrace();
+            }
+            ClassIdValidationJob.execute(0); // the modification of the annotation does not cause an annotation modify event to be triggered
+          }
+        });
+        j.schedule();
+      }
+    }
+  }
+
+  private IOperation createUpdateClassIdInDocumentationOperation(final INlsEntry nlsEntry, final String newId) {
+    return new IOperation() {
+
+      @Override
+      public void validate() throws IllegalArgumentException {
+      }
+
+      @Override
+      public void run(IProgressMonitor monitor, IWorkingCopyManager workingCopyManager) throws CoreException, IllegalArgumentException {
+        nlsEntry.getProject().updateKey(nlsEntry, newId, monitor);
+      }
+
+      @Override
+      public String getOperationName() {
+        return "Update NLS Key to new ClassId";
+      }
+    };
+  }
+
+  private IOperation createUpdateAnnotationInJavaSourceOperation(IType annotationOwner, String newId) {
+    return new AnnotationNewOperation(AnnotationSourceBuilderFactory.createClassIdAnnotation(newId), annotationOwner);
+  }
+}
diff --git a/org.eclipse.scout.sdk.ui/src/org/eclipse/scout/sdk/ui/classid/ClassIdDuplicateResolutionGenerator.java b/org.eclipse.scout.sdk.ui/src/org/eclipse/scout/sdk/ui/classid/ClassIdDuplicateResolutionGenerator.java
new file mode 100644
index 0000000..8191ea7
--- /dev/null
+++ b/org.eclipse.scout.sdk.ui/src/org/eclipse/scout/sdk/ui/classid/ClassIdDuplicateResolutionGenerator.java
@@ -0,0 +1,36 @@
+package org.eclipse.scout.sdk.ui.classid;
+
+import org.eclipse.core.resources.IMarker;
+import org.eclipse.jdt.core.IAnnotation;
+import org.eclipse.scout.sdk.classid.ClassIdValidationJob;
+import org.eclipse.scout.sdk.ui.internal.ScoutSdkUi;
+import org.eclipse.ui.IMarkerResolution;
+import org.eclipse.ui.IMarkerResolutionGenerator;
+
+/**
+ * <h3>{@link ClassIdDuplicateResolutionGenerator}</h3>
+ * 
+ * @author Matthias Villiger
+ * @since 4.0.0 21.05.2014
+ */
+public class ClassIdDuplicateResolutionGenerator implements IMarkerResolutionGenerator {
+
+  @Override
+  public IMarkerResolution[] getResolutions(IMarker marker) {
+    try {
+      if (marker != null && marker.exists() && ClassIdValidationJob.CLASS_ID_DUPLICATE_MARKER_ID.equals(marker.getType())) {
+        Object annot = marker.getAttribute(ClassIdValidationJob.CLASS_ID_ATTR_ANNOTATION);
+        if (annot instanceof IAnnotation) {
+          IAnnotation annotation = (IAnnotation) annot;
+          if (annotation.exists()) {
+            return new IMarkerResolution[]{new ClassIdDuplicateResolution(annotation)};
+          }
+        }
+      }
+    }
+    catch (Exception e) {
+      ScoutSdkUi.logError("Unable to calculate possible marker resolutions.", e);
+    }
+    return new IMarkerResolution[]{};
+  }
+}
diff --git a/org.eclipse.scout.sdk.ui/src/org/eclipse/scout/sdk/ui/view/outline/pages/InnerTypeOrderChangedPageDirtyListener.java b/org.eclipse.scout.sdk.ui/src/org/eclipse/scout/sdk/ui/view/outline/pages/InnerTypeOrderChangedPageDirtyListener.java
index fd5a8c8..fec363c 100644
--- a/org.eclipse.scout.sdk.ui/src/org/eclipse/scout/sdk/ui/view/outline/pages/InnerTypeOrderChangedPageDirtyListener.java
+++ b/org.eclipse.scout.sdk.ui/src/org/eclipse/scout/sdk/ui/view/outline/pages/InnerTypeOrderChangedPageDirtyListener.java
@@ -35,8 +35,8 @@
 
   @Override
   public void handleEvent(JdtEvent event) {
-    if (TypeUtility.exists(event.getElement())) {
-      if (event.getElementType() == IJavaElement.ANNOTATION) {
+    if (event.getElementType() == IJavaElement.ANNOTATION) {
+      if (TypeUtility.exists(event.getElement())) {
         IAnnotation annotation = (IAnnotation) event.getElement();
         IJavaElement annotationOwner = annotation.getParent();
         ITypeHierarchy superTypeHierarchy = event.getSuperTypeHierarchy();
diff --git a/org.eclipse.scout.sdk.util/src/org/eclipse/scout/sdk/util/internal/typecache/HierarchyCache.java b/org.eclipse.scout.sdk.util/src/org/eclipse/scout/sdk/util/internal/typecache/HierarchyCache.java
index c29649a..afe5645 100644
--- a/org.eclipse.scout.sdk.util/src/org/eclipse/scout/sdk/util/internal/typecache/HierarchyCache.java
+++ b/org.eclipse.scout.sdk.util/src/org/eclipse/scout/sdk/util/internal/typecache/HierarchyCache.java
@@ -288,9 +288,11 @@
         }
         else if (e.getElementType() == IJavaElement.COMPILATION_UNIT) {
           try {
-            IType[] types = ((ICompilationUnit) e.getElement()).getTypes();
-            if (types.length > 0) {
-              handleTypeChange(types[0], e.getSuperTypeHierarchy());
+            if (TypeUtility.exists(e.getElement())) {
+              IType[] types = ((ICompilationUnit) e.getElement()).getTypes();
+              if (types.length > 0) {
+                handleTypeChange(types[0], e.getSuperTypeHierarchy());
+              }
             }
           }
           catch (JavaModelException ex) {
diff --git a/org.eclipse.scout.sdk.util/src/org/eclipse/scout/sdk/util/resources/ResourceProxy.java b/org.eclipse.scout.sdk.util/src/org/eclipse/scout/sdk/util/resources/ResourceProxy.java
new file mode 100644
index 0000000..782bc48
--- /dev/null
+++ b/org.eclipse.scout.sdk.util/src/org/eclipse/scout/sdk/util/resources/ResourceProxy.java
@@ -0,0 +1,98 @@
+/*******************************************************************************
+ * Copyright (c) 2013 BSI Business Systems Integration AG.
+ * 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:
+ *     BSI Business Systems Integration AG - initial API and implementation
+ ******************************************************************************/
+package org.eclipse.scout.sdk.util.resources;
+
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.resources.IResourceProxy;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.core.runtime.QualifiedName;
+import org.eclipse.scout.sdk.util.internal.SdkUtilActivator;
+
+/**
+ * <h3>{@link ResourceProxy}</h3>
+ * 
+ * @author Matthias Villiger
+ * @since 4.0.0 20.05.2014
+ */
+public class ResourceProxy implements IResourceProxy {
+  private final IResource m_resource;
+
+  public ResourceProxy(IResource r) {
+    m_resource = r;
+  }
+
+  @Override
+  public long getModificationStamp() {
+    return m_resource.getModificationStamp();
+  }
+
+  @Override
+  public boolean isAccessible() {
+    return m_resource.isAccessible();
+  }
+
+  @Override
+  public boolean isDerived() {
+    return m_resource.isDerived();
+  }
+
+  @Override
+  public boolean isLinked() {
+    return m_resource.isLinked();
+  }
+
+  @Override
+  public boolean isPhantom() {
+    return m_resource.isPhantom();
+  }
+
+  @Override
+  public boolean isHidden() {
+    return m_resource.isHidden();
+  }
+
+  @Override
+  public boolean isTeamPrivateMember() {
+    return m_resource.isTeamPrivateMember();
+  }
+
+  @Override
+  public String getName() {
+    return m_resource.getName();
+  }
+
+  @Override
+  public Object getSessionProperty(QualifiedName key) {
+    try {
+      return m_resource.getSessionProperty(key);
+    }
+    catch (CoreException e) {
+      SdkUtilActivator.logError("unable to retrieve session property '" + key.toString() + "' from resource '" + m_resource.getFullPath().toOSString() + "'.", e);
+      return null;
+    }
+  }
+
+  @Override
+  public int getType() {
+    return m_resource.getType();
+  }
+
+  @Override
+  public IPath requestFullPath() {
+    return m_resource.getFullPath();
+  }
+
+  @Override
+  public IResource requestResource() {
+    return m_resource;
+  }
+}
diff --git a/org.eclipse.scout.sdk.util/src/org/eclipse/scout/sdk/util/resources/ResourceUtility.java b/org.eclipse.scout.sdk.util/src/org/eclipse/scout/sdk/util/resources/ResourceUtility.java
index 271e599..c861336 100644
--- a/org.eclipse.scout.sdk.util/src/org/eclipse/scout/sdk/util/resources/ResourceUtility.java
+++ b/org.eclipse.scout.sdk.util/src/org/eclipse/scout/sdk/util/resources/ResourceUtility.java
@@ -32,6 +32,8 @@
 import org.eclipse.core.resources.IFile;
 import org.eclipse.core.resources.IFolder;
 import org.eclipse.core.resources.IResource;
+import org.eclipse.core.resources.IResourceDelta;
+import org.eclipse.core.resources.IResourceDeltaVisitor;
 import org.eclipse.core.resources.IResourceProxy;
 import org.eclipse.core.resources.IResourceProxyVisitor;
 import org.eclipse.core.resources.ProjectScope;
@@ -78,7 +80,7 @@
       @Override
       public boolean visit(IResourceProxy proxy) throws CoreException {
         if (proxy.isAccessible()) {
-          if (filter.accept(proxy)) {
+          if (filter == null || filter.accept(proxy)) {
             collector.add(proxy.requestResource());
           }
           return true;
@@ -90,6 +92,27 @@
     return collector.toArray(new IResource[collector.size()]);
   }
 
+  public static List<IResource> getAllResources(IResourceDelta d, final IResourceFilter filter) throws CoreException {
+    final List<IResource> result = new LinkedList<IResource>();
+    if (d == null) {
+      return result;
+    }
+
+    d.accept(new IResourceDeltaVisitor() {
+      @Override
+      public boolean visit(IResourceDelta delta) throws CoreException {
+        IResource resource = delta.getResource();
+        if (resource.isAccessible()) {
+          if (filter == null || filter.accept(new ResourceProxy(resource))) {
+            result.add(resource);
+          }
+        }
+        return true;
+      }
+    });
+    return result;
+  }
+
   /**
    * @return The {@link IFile} defining the currently active target platform or null if no such file exists in the
    *         workspace.
diff --git a/org.eclipse.scout.sdk/META-INF/MANIFEST.MF b/org.eclipse.scout.sdk/META-INF/MANIFEST.MF
index 71a50b0..c6b6c8b 100644
--- a/org.eclipse.scout.sdk/META-INF/MANIFEST.MF
+++ b/org.eclipse.scout.sdk/META-INF/MANIFEST.MF
@@ -11,6 +11,7 @@
  org.eclipse.scout.sdk.util;bundle-version="[4.0.0,4.1.0)";visibility:=reexport,
  org.eclipse.scout.nls.sdk;bundle-version="[4.0.0,4.1.0)";visibility:=reexport
 Export-Package: org.eclipse.scout.sdk,
+ org.eclipse.scout.sdk.classid,
  org.eclipse.scout.sdk.extensions.classidgenerators,
  org.eclipse.scout.sdk.extensions.codeid,
  org.eclipse.scout.sdk.extensions.codeid.parsers,
diff --git a/org.eclipse.scout.sdk/plugin.xml b/org.eclipse.scout.sdk/plugin.xml
index d3b5771..59e8248 100644
--- a/org.eclipse.scout.sdk/plugin.xml
+++ b/org.eclipse.scout.sdk/plugin.xml
@@ -119,6 +119,23 @@
       </attribute>
    </extension>
    <extension
+         id="org.eclipse.scout.sdk.classid.duplicate"
+         name="ClassId Duplicate"
+         point="org.eclipse.core.resources.markers">
+      <persistent
+            value="false">
+      </persistent>
+      <super
+            type="org.eclipse.core.resources.marker">
+      </super>
+      <super
+            type="org.eclipse.core.resources.textmarker">
+      </super>
+      <super
+            type="org.eclipse.core.resources.problemmarker">
+      </super>
+   </extension>
+   <extension
          id="org.eclipse.scout.sdk.formdata.sql.binding.multi"
          name="SQL Binding Multi"
          point="org.eclipse.core.resources.markers">
diff --git a/org.eclipse.scout.sdk/resources/texts/Texts.properties b/org.eclipse.scout.sdk/resources/texts/Texts.properties
index 3746a89..5c67ec5 100644
--- a/org.eclipse.scout.sdk/resources/texts/Texts.properties
+++ b/org.eclipse.scout.sdk/resources/texts/Texts.properties
@@ -192,6 +192,7 @@
 Down=Down
 DownloadEclipsePlatformAsWell=Download a new Luna Eclipse platform as well.
 DownloadRAPTarget=Download RAP Target
+DuplicateClassIdValue=Duplicate @ClassId value '{0}' in type '{1}'.
 EarFileName=EAR File Name
 EclipsePlatform=Eclipse Platform
 Edit=Edit
@@ -278,6 +279,7 @@
 ItemsFound=items found
 JARFileSelection=JAR file selection
 KEY=KEY
+KeepAssignedDocumentation=Migrate assigned documentation?
 KeepCurrentTarget=Keep current Target ({0})
 KeyStroke=Key Stroke
 KeyStrokesTablePage=Keystrokes
@@ -596,6 +598,7 @@
 TheRAPTargetWillBeDownloaded=The RAP target will be downloaded. This can take some minutes and requires Internet access.
 TheSessionClassCanNotBeNull=Please choose a Session Class.
 TheSuperTypeCanNotBeNull=Please choose a super class.
+ThisClassIdHasADocumentationEntryAssigned=This @ClassId has a documentation entry assigned.\r\nWould you like to update this entry to the new value?\r\n\r\nIf yes, the existing documentation entry will be updated with the new @ClassId value this class will receive.\r\nIf no, the existing documentation entry will not be changed (and will therefore still point to other having the same value). This class will then no longer have a documentation.
 ToolButtonTemplates=Tool Button Templates
 ToolTablePage=Tools
 Type=Type
@@ -612,6 +615,7 @@
 UpdatePageData=Update PageData
 UpdatePermission=UpdatePermission
 UpdatePresenterForX=Update Presenter for '{0}'...
+UpdateWithNewClassIdValue=Update with new @ClassId value
 UseDefault=use default
 UseDefaultScoutJDTPreferences=Use default Scout JDT preferences
 UseLegacyTargetPackage=Use Legacy Target Package
diff --git a/org.eclipse.scout.sdk/src/org/eclipse/scout/sdk/classid/ClassIdValidationJob.java b/org.eclipse.scout.sdk/src/org/eclipse/scout/sdk/classid/ClassIdValidationJob.java
new file mode 100644
index 0000000..4890bbd
--- /dev/null
+++ b/org.eclipse.scout.sdk/src/org/eclipse/scout/sdk/classid/ClassIdValidationJob.java
@@ -0,0 +1,262 @@
+/*******************************************************************************
+ * Copyright (c) 2013 BSI Business Systems Integration AG.
+ * 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:
+ *     BSI Business Systems Integration AG - initial API and implementation
+ ******************************************************************************/
+package org.eclipse.scout.sdk.classid;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+
+import org.eclipse.core.resources.IMarker;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.resources.ResourcesPlugin;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.OperationCanceledException;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.core.runtime.jobs.ISchedulingRule;
+import org.eclipse.core.runtime.jobs.Job;
+import org.eclipse.jdt.core.IAnnotation;
+import org.eclipse.jdt.core.IJavaElement;
+import org.eclipse.jdt.core.ISourceRange;
+import org.eclipse.jdt.core.IType;
+import org.eclipse.jdt.core.Signature;
+import org.eclipse.jdt.core.search.IJavaSearchConstants;
+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.TypeReferenceMatch;
+import org.eclipse.jface.text.BadLocationException;
+import org.eclipse.jface.text.Document;
+import org.eclipse.scout.commons.StringUtility;
+import org.eclipse.scout.commons.job.JobEx;
+import org.eclipse.scout.sdk.ScoutSdkCore;
+import org.eclipse.scout.sdk.Texts;
+import org.eclipse.scout.sdk.extensions.runtime.classes.IRuntimeClasses;
+import org.eclipse.scout.sdk.internal.ScoutSdk;
+import org.eclipse.scout.sdk.util.jdt.IJavaResourceChangedListener;
+import org.eclipse.scout.sdk.util.jdt.JdtEvent;
+import org.eclipse.scout.sdk.util.jdt.JdtUtility;
+import org.eclipse.scout.sdk.util.log.ScoutStatus;
+import org.eclipse.scout.sdk.util.type.TypeUtility;
+
+/**
+ * <h3>{@link ClassIdValidationJob}</h3>
+ * 
+ * @author Matthias Villiger
+ * @since 4.0.0 20.05.2014
+ */
+public final class ClassIdValidationJob extends JobEx {
+
+  public static final String CLASS_ID_VALIDATION_JOB_FAMILY = "CLASS_ID_VALIDATION_JOB_FAMILY";
+  public static final String CLASS_ID_DUPLICATE_MARKER_ID = "org.eclipse.scout.sdk.classid.duplicate";
+  public static final String CLASS_ID_ATTR_ANNOTATION = "SCOUT_CLASS_ID_ATTR_ANNOTATION";
+
+  private static IJavaResourceChangedListener listener;
+
+  private ClassIdValidationJob() {
+    super(ClassIdValidationJob.class.getName());
+    setSystem(true);
+    setUser(false);
+    setRule(new P_SchedulingRule());
+  }
+
+  private Set<IAnnotation> getAllClassIdAnnotationsInWorkspace(IProgressMonitor monitor) {
+    SearchEngine e = new SearchEngine();
+    IType classId = TypeUtility.getType(IRuntimeClasses.ClassId);
+    final HashSet<IAnnotation> result = new HashSet<IAnnotation>();
+    try {
+      e.search(SearchPattern.createPattern(classId, IJavaSearchConstants.ANNOTATION_TYPE_REFERENCE, SearchPattern.R_EXACT_MATCH),
+          new SearchParticipant[]{SearchEngine.getDefaultSearchParticipant()},
+          SearchEngine.createWorkspaceScope(), new SearchRequestor() {
+            @Override
+            public void acceptSearchMatch(SearchMatch match) throws CoreException {
+              IJavaElement element = ((TypeReferenceMatch) match).getLocalElement();
+              if (element == null) {
+                // e.g. when the annotation is fully qualified. try reading from owner
+                Object owner = match.getElement();
+                if (owner instanceof IType) {
+                  IType ownerType = (IType) owner;
+                  if (!ownerType.isBinary() && TypeUtility.exists(ownerType)) {
+                    element = JdtUtility.getAnnotation(ownerType, IRuntimeClasses.ClassId);
+                  }
+                }
+              }
+
+              if (element instanceof IAnnotation && !element.isReadOnly() && TypeUtility.exists(element)) {
+                result.add((IAnnotation) element);
+              }
+            }
+          }, monitor);
+    }
+    catch (OperationCanceledException oce) {
+      //nop
+    }
+    catch (CoreException ex) {
+      ScoutSdk.logError("unable to find @ClassId annotation references in workspace.", ex);
+    }
+    return result;
+  }
+
+  @Override
+  public boolean belongsTo(Object family) {
+    return CLASS_ID_VALIDATION_JOB_FAMILY.equals(family);
+  }
+
+  public synchronized static void install() {
+    if (listener == null) {
+      listener = new P_ResourceChangeListener();
+      ScoutSdkCore.getJavaResourceChangedEmitter().addJavaResourceChangedListener(listener);
+    }
+  }
+
+  public synchronized static void uninstall() {
+    if (listener != null) {
+      ScoutSdkCore.getJavaResourceChangedEmitter().removeJavaResourceChangedListener(listener);
+    }
+  }
+
+  private Map<String /*classid*/, List<IAnnotation>> getClassIdOccurrences(IProgressMonitor monitor) throws CoreException {
+    Map<String, List<IAnnotation>> ids = new HashMap<String, List<IAnnotation>>();
+    Set<IAnnotation> allClassIdAnnotationsInWorkspace = getAllClassIdAnnotationsInWorkspace(monitor);
+    if (monitor.isCanceled()) {
+      return null;
+    }
+
+    for (IAnnotation r : allClassIdAnnotationsInWorkspace) {
+      if (monitor.isCanceled()) {
+        return null;
+      }
+
+      if (TypeUtility.exists(r)) {
+        String id = JdtUtility.getAnnotationValueString(r, "value");
+        if (!StringUtility.isNullOrEmpty(id)) {
+          List<IAnnotation> files = ids.get(id);
+          if (files == null) {
+            files = new LinkedList<IAnnotation>();
+            ids.put(id, files);
+          }
+          files.add(r);
+        }
+      }
+    }
+    return ids;
+  }
+
+  private Set<IAnnotation> getVisibleClassIds(IAnnotation current, List<IAnnotation> matchesById) {
+    Set<IAnnotation> visibleMatches = new HashSet<IAnnotation>(matchesById.size());
+    for (IAnnotation m : matchesById) {
+      if (m != current && TypeUtility.isOnClasspath(m, current.getJavaProject())) {
+        visibleMatches.add(m);
+      }
+    }
+    return visibleMatches;
+  }
+
+  private void createDuplicateMarkers(Map<String, List<IAnnotation>> annotations) throws CoreException {
+    for (Entry<String, List<IAnnotation>> matches : annotations.entrySet()) {
+      List<IAnnotation> matchesById = matches.getValue();
+      if (matchesById.size() > 1) {
+        for (IAnnotation duplicate : matchesById) {
+          IType parent = (IType) duplicate.getAncestor(IJavaElement.TYPE);
+          if (TypeUtility.exists(parent)) {
+            // duplicate found: check if they can see each others
+            Set<IAnnotation> visibleDuplicates = getVisibleClassIds(duplicate, matchesById);
+            if (visibleDuplicates.size() > 0) {
+              ISourceRange sourceRange = duplicate.getSourceRange();
+              if (sourceRange != null && sourceRange.getOffset() >= 0) {
+                IMarker marker = duplicate.getResource().createMarker(CLASS_ID_DUPLICATE_MARKER_ID);
+                marker.setAttribute(IMarker.MESSAGE, Texts.get("DuplicateClassIdValue", matches.getKey(), parent.getFullyQualifiedName()));
+                marker.setAttribute(IMarker.PRIORITY, IMarker.PRIORITY_HIGH);
+                marker.setAttribute(IMarker.CHAR_START, sourceRange.getOffset());
+                marker.setAttribute(IMarker.CHAR_END, sourceRange.getOffset() + sourceRange.getLength());
+                marker.setAttribute(IMarker.SEVERITY, IMarker.SEVERITY_ERROR);
+                try {
+                  Document doc = new Document(parent.getCompilationUnit().getSource());
+                  marker.setAttribute(IMarker.LINE_NUMBER, doc.getLineOfOffset(sourceRange.getOffset()) + 1);
+                }
+                catch (BadLocationException e) {
+                  //nop
+                }
+                marker.setAttribute(CLASS_ID_ATTR_ANNOTATION, duplicate);
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+
+  private void deleteDuplicateMarkers() {
+    try {
+      ResourcesPlugin.getWorkspace().getRoot().deleteMarkers(CLASS_ID_DUPLICATE_MARKER_ID, true, IResource.DEPTH_INFINITE);
+    }
+    catch (CoreException e) {
+      ScoutSdk.logError("unable to remove old class id duplicate markers", e);
+    }
+  }
+
+  @Override
+  protected IStatus run(IProgressMonitor monitor) {
+    try {
+      Map<String, List<IAnnotation>> classIdOccurrences = getClassIdOccurrences(monitor);
+      if (monitor.isCanceled()) {
+        return Status.CANCEL_STATUS;
+      }
+
+      deleteDuplicateMarkers();
+      createDuplicateMarkers(classIdOccurrences);
+      return Status.OK_STATUS;
+    }
+    catch (Exception e) {
+      ScoutSdk.logError("", e);
+      return new ScoutStatus("Error while updating class id duplicate markers.", e);
+    }
+  }
+
+  private static final class P_ResourceChangeListener implements IJavaResourceChangedListener {
+    @Override
+    public void handleEvent(JdtEvent event) {
+      if (event.getElementType() == IJavaElement.ANNOTATION) {
+        IAnnotation annotation = (IAnnotation) event.getElement();
+        if (annotation != null && annotation.getElementName().endsWith(Signature.getSimpleName(IRuntimeClasses.ClassId))) {
+          if (TypeUtility.exists(event.getElement())) {
+            execute(0);
+          }
+        }
+      }
+    }
+  }
+
+  public static synchronized void execute(long startDelay) {
+    Job.getJobManager().cancel(CLASS_ID_VALIDATION_JOB_FAMILY);
+    new ClassIdValidationJob().schedule(startDelay);
+  }
+
+  private static final class P_SchedulingRule implements ISchedulingRule {
+
+    @Override
+    public boolean contains(ISchedulingRule rule) {
+      return rule instanceof P_SchedulingRule;
+    }
+
+    @Override
+    public boolean isConflicting(ISchedulingRule rule) {
+      return rule instanceof P_SchedulingRule;
+    }
+  }
+}
diff --git a/org.eclipse.scout.sdk/src/org/eclipse/scout/sdk/internal/ScoutSdk.java b/org.eclipse.scout.sdk/src/org/eclipse/scout/sdk/internal/ScoutSdk.java
index 08b1ae4..bbec9ca 100644
--- a/org.eclipse.scout.sdk/src/org/eclipse/scout/sdk/internal/ScoutSdk.java
+++ b/org.eclipse.scout.sdk/src/org/eclipse/scout/sdk/internal/ScoutSdk.java
@@ -14,6 +14,7 @@
 import org.eclipse.core.runtime.Plugin;
 import org.eclipse.scout.commons.TuningUtility;
 import org.eclipse.scout.sdk.ScoutSdkCore;
+import org.eclipse.scout.sdk.classid.ClassIdValidationJob;
 import org.eclipse.scout.sdk.internal.workspace.ScoutWorkspace;
 import org.eclipse.scout.sdk.internal.workspace.dto.DtoAutoUpdateManager;
 import org.eclipse.scout.sdk.internal.workspace.dto.formdata.FormDataDtoUpdateHandler;
@@ -37,16 +38,22 @@
   @Override
   public void start(BundleContext context) throws Exception {
     super.start(context);
+
     plugin = this;
     logManager = new SdkLogManager(this);
 
+    logInfo("Starting SCOUT SDK Plugin.");
+
     // ensure the caches and emitters are initialized.
     ScoutSdkCore.getHierarchyCache();
     ScoutSdkCore.getTypeCache();
     ScoutSdkCore.getJavaResourceChangedEmitter();
 
-    logInfo("Starting SCOUT SDK Plugin.");
+    // start class id validation
+    ClassIdValidationJob.install();
+    ClassIdValidationJob.execute(5000);
 
+    // DTO auto update
     m_autoUpdateManager = new DtoAutoUpdateManager();
     m_autoUpdateManager.addModelDataUpdateHandler(new FormDataDtoUpdateHandler());
     m_autoUpdateManager.addModelDataUpdateHandler(new PageDataAutoUpdateHandler());
@@ -56,6 +63,7 @@
   public void stop(BundleContext context) throws Exception {
     TuningUtility.finishAll();
     m_autoUpdateManager.dispose();
+    ClassIdValidationJob.uninstall();
     ScoutWorkspace.getInstance().dispose();
     ScoutSdkCore.getHierarchyCache().dispose();
     ScoutSdkCore.getTypeCache().dispose();
diff --git a/org.eclipse.scout.sdk/src/org/eclipse/scout/sdk/internal/workspace/ScoutWorkspace.java b/org.eclipse.scout.sdk/src/org/eclipse/scout/sdk/internal/workspace/ScoutWorkspace.java
index 5dc31af..3b57226 100644
--- a/org.eclipse.scout.sdk/src/org/eclipse/scout/sdk/internal/workspace/ScoutWorkspace.java
+++ b/org.eclipse.scout.sdk/src/org/eclipse/scout/sdk/internal/workspace/ScoutWorkspace.java
@@ -81,35 +81,10 @@
       P_BundleGraphRebuildShutdownJob job = new P_BundleGraphRebuildShutdownJob();
       job.schedule();
       try {
-        job.join(3000);
+        job.join(20000);
       }
       catch (InterruptedException e) {
       }
-      if (job.getResult() == null) {
-        // the job is still waiting for the shutdown -> enforce it
-        Job[] jobs = Job.getJobManager().find(BUNDLE_GRAPH_REBUILD_JOB_FAMILY);
-        if (jobs != null && jobs.length > 0) {
-          for (Job j : jobs) {
-            if (j != null) {
-              Thread t = j.getThread();
-              if (t != null) {
-                t.interrupt();
-                try {
-                  j.join();
-                }
-                catch (InterruptedException e) {
-                }
-              }
-            }
-          }
-        }
-
-        try {
-          job.join(3000);
-        }
-        catch (InterruptedException e1) {
-        }
-      }
 
       m_bundleGraph.dispose();
       m_eventListeners = new EventListenerList(); // loose all old listeners
@@ -201,7 +176,7 @@
         l.workspaceChanged(e);
       }
       catch (Exception t) {
-        ScoutSdk.logError("error during listener notification.", t);
+        ScoutSdk.logError("error during listener notification '" + l.getClass().getName() + "'.", t);
       }
     }
   }
@@ -221,7 +196,7 @@
     }
   }
 
-  private final class P_BundleGraphRebuildJob extends Job {
+  private final class P_BundleGraphRebuildJob extends JobEx {
 
     private final ScoutWorkspaceEventList m_eventCollector;
 
diff --git a/org.eclipse.scout.sdk/src/org/eclipse/scout/sdk/jdt/compile/CompileResult.java b/org.eclipse.scout.sdk/src/org/eclipse/scout/sdk/jdt/compile/CompileResult.java
index 2f82aea..9b4c0b6 100644
--- a/org.eclipse.scout.sdk/src/org/eclipse/scout/sdk/jdt/compile/CompileResult.java
+++ b/org.eclipse.scout.sdk/src/org/eclipse/scout/sdk/jdt/compile/CompileResult.java
@@ -11,7 +11,6 @@
 package org.eclipse.scout.sdk.jdt.compile;
 
 import java.util.ArrayList;
-import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import java.util.TreeMap;
@@ -22,7 +21,7 @@
 /**
  * <h3>{@link CompileResult}</h3> ...
  * 
- *  @author Andreas Hoegger
+ * @author Andreas Hoegger
  * @since 3.9.0 18.03.2013
  */
 public class CompileResult implements ICompileResult {
@@ -51,7 +50,7 @@
         errorMarkers.add(m);
       }
     }
-    return Collections.unmodifiableList(errorMarkers);
+    return errorMarkers;
   }
 
   @Override
diff --git a/org.eclipse.scout.sdk/src/org/eclipse/scout/sdk/jdt/compile/ScoutSeverityManager.java b/org.eclipse.scout.sdk/src/org/eclipse/scout/sdk/jdt/compile/ScoutSeverityManager.java
index f826fff..f4b8ce8 100644
--- a/org.eclipse.scout.sdk/src/org/eclipse/scout/sdk/jdt/compile/ScoutSeverityManager.java
+++ b/org.eclipse.scout.sdk/src/org/eclipse/scout/sdk/jdt/compile/ScoutSeverityManager.java
@@ -48,7 +48,7 @@
       public void resourceChanged(IResourceChangeEvent e) {
         IMarkerDelta[] mdeltas = e.findMarkerDeltas(IMarker.PROBLEM, true);
         if (mdeltas != null && mdeltas.length > 0) {
-          HashSet<IResource> changedResorces = new HashSet<IResource>();
+          HashSet<IResource> changedResorces = new HashSet<IResource>(mdeltas.length);
           for (IMarkerDelta d : mdeltas) {
             IResource r = d.getMarker().getResource();
             if (r != null) {
@@ -132,6 +132,7 @@
           case IJavaElement.INITIALIZER:
           case IJavaElement.METHOD:
           case IJavaElement.FIELD:
+          case IJavaElement.ANNOTATION:
           case IJavaElement.LOCAL_VARIABLE:
             ICompilationUnit cu = (ICompilationUnit) element.getAncestor(IJavaElement.COMPILATION_UNIT);
             if (cu != null) {