[bugzilla 411217] Make the WorkspaceSession able to identify external modifications (e.g. git reset), so that external changes can be taken into account inside editor editors & clients

Also add the corresponding test
diff --git a/plugins/org.eclipse.mylyn.docs.intent.collab.ide/src/org/eclipse/mylyn/docs/intent/collab/ide/adapters/WorkspaceAdapter.java b/plugins/org.eclipse.mylyn.docs.intent.collab.ide/src/org/eclipse/mylyn/docs/intent/collab/ide/adapters/WorkspaceAdapter.java
index 694bf5b..f0afe48 100644
--- a/plugins/org.eclipse.mylyn.docs.intent.collab.ide/src/org/eclipse/mylyn/docs/intent/collab/ide/adapters/WorkspaceAdapter.java
+++ b/plugins/org.eclipse.mylyn.docs.intent.collab.ide/src/org/eclipse/mylyn/docs/intent/collab/ide/adapters/WorkspaceAdapter.java
@@ -29,6 +29,7 @@
 import java.util.Set;
 
 import org.eclipse.emf.common.command.CommandStack;
+import org.eclipse.emf.common.notify.impl.AdapterImpl;
 import org.eclipse.emf.common.util.URI;
 import org.eclipse.emf.ecore.EObject;
 import org.eclipse.emf.ecore.EStructuralFeature;
@@ -232,8 +233,11 @@
 								// Step 2: we send a warning to the WorkspaceSession if necessary
 								treatSessionWarning(resource);
 
-								// Step 3: save the resource
+								// Step 3: add an adapter used by the WorkspaceSession to distinguish internal
+								// from external modifications
+								resource.eAdapters().add(new InternalModificationAdapter());
 
+								// Step 4: save the resource
 								if (resource.getContents().isEmpty()) {
 									// if the resource is empty, we delete it
 									resource.delete(getSaveOptions());
@@ -559,8 +563,11 @@
 	public Object getIDFromElement(EObject element) {
 		URI uri = null;
 		if (element != null) {
-			uri = EcoreUtil.getURI(element);
-
+			if (element.eResource() != null && element.eResource().getContents().contains(element)) {
+				uri = URI.createURI(element.eResource().getURI().toString() + "#/");
+			} else {
+				uri = EcoreUtil.getURI(element);
+			}
 			// if the URI starts with "#", it means that the resource is no longer part of the resource set.
 			// we return null to indicate the the given element is now invalid
 			if (uri.toString().startsWith("#")) {
@@ -757,4 +764,19 @@
 	public ResourceSet getResourceSet() {
 		return new ResourceSetImpl();
 	}
+
+	/**
+	 * An adapter used to identify resources that are saved by a WorkspaceAdapter (and differentiate internal
+	 * from external modifications).
+	 * 
+	 * @author <a href="mailto:alex.lagarde@obeo.fr">Alex Lagarde</a>
+	 */
+	public static final class InternalModificationAdapter extends AdapterImpl {
+
+		/**
+		 * Default constructor.
+		 */
+		private InternalModificationAdapter() {
+		}
+	}
 }
diff --git a/plugins/org.eclipse.mylyn.docs.intent.collab.ide/src/org/eclipse/mylyn/docs/intent/collab/ide/repository/WorkspaceRepositoryLoader.java b/plugins/org.eclipse.mylyn.docs.intent.collab.ide/src/org/eclipse/mylyn/docs/intent/collab/ide/repository/WorkspaceRepositoryLoader.java
index 55a61b6..e9cc93d 100644
--- a/plugins/org.eclipse.mylyn.docs.intent.collab.ide/src/org/eclipse/mylyn/docs/intent/collab/ide/repository/WorkspaceRepositoryLoader.java
+++ b/plugins/org.eclipse.mylyn.docs.intent.collab.ide/src/org/eclipse/mylyn/docs/intent/collab/ide/repository/WorkspaceRepositoryLoader.java
@@ -10,7 +10,6 @@
  *******************************************************************************/
 package org.eclipse.mylyn.docs.intent.collab.ide.repository;
 
-import java.io.IOException;
 import java.util.List;
 
 import org.eclipse.emf.common.util.URI;
@@ -70,11 +69,7 @@
 				workspaceRepository.getResourceSet().getResource(fileURI, true);
 			}
 		} catch (WrappedException e) {
-			try {
-				workspaceRepository.getResourceSet().createResource(fileURI).save(null);
-			} catch (IOException e1) {
-				// Silently fail
-			}
+			// Silent catch: resource will not be added to the resource set
 		}
 	}
 
diff --git a/plugins/org.eclipse.mylyn.docs.intent.collab.ide/src/org/eclipse/mylyn/docs/intent/collab/ide/repository/WorkspaceSession.java b/plugins/org.eclipse.mylyn.docs.intent.collab.ide/src/org/eclipse/mylyn/docs/intent/collab/ide/repository/WorkspaceSession.java
index 99cd161..b2f0ba1 100644
--- a/plugins/org.eclipse.mylyn.docs.intent.collab.ide/src/org/eclipse/mylyn/docs/intent/collab/ide/repository/WorkspaceSession.java
+++ b/plugins/org.eclipse.mylyn.docs.intent.collab.ide/src/org/eclipse/mylyn/docs/intent/collab/ide/repository/WorkspaceSession.java
@@ -17,6 +17,7 @@
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.Iterator;
 import java.util.List;
 
 import org.eclipse.core.resources.IResource;
@@ -29,12 +30,12 @@
 import org.eclipse.core.runtime.Status;
 import org.eclipse.core.runtime.jobs.Job;
 import org.eclipse.emf.common.notify.Adapter;
-import org.eclipse.emf.common.util.WrappedException;
 import org.eclipse.emf.ecore.EObject;
 import org.eclipse.emf.ecore.resource.Resource;
 import org.eclipse.mylyn.docs.intent.collab.handlers.adapters.IntentCommand;
 import org.eclipse.mylyn.docs.intent.collab.handlers.impl.notification.elementList.ElementListAdapter;
 import org.eclipse.mylyn.docs.intent.collab.ide.adapters.WorkspaceAdapter;
+import org.eclipse.mylyn.docs.intent.collab.ide.adapters.WorkspaceAdapter.InternalModificationAdapter;
 import org.eclipse.mylyn.docs.intent.collab.ide.notification.WorkspaceTypeListener;
 
 /**
@@ -140,26 +141,39 @@
 				// We get the changed and removed Resources
 				Collection<Resource> removedResources = new ArrayList<Resource>();
 				Collection<Resource> changedResources = new ArrayList<Resource>();
+				Collection<Resource> changedResourcesToReload = new ArrayList<Resource>();
 
 				if (!visitor.getRemovedResources().isEmpty()) {
 					removedResources.addAll(visitor.getRemovedResources());
 				}
 
 				for (Resource changedResource : visitor.getChangedResources()) {
+					Iterator<InternalModificationAdapter> internalModificationAdapters = Iterables.filter(
+							changedResource.eAdapters(), InternalModificationAdapter.class).iterator();
 
 					// If the resource is contained in the savedResources list, it means
 					// that we should ignore this notification ; however we remove this resource
 					// from this list so that we'll treat the next notifications
 					if (!savedResources.contains(changedResource)) {
 						changedResources.add(changedResource);
+
+						// If the resource is not associated to any InternalModificationAdapter, it means that
+						// it has been saved without using the Workspace, and hence that we should reload the
+						// resource to handle this external modification
+						if (!internalModificationAdapters.hasNext()) {
+							changedResourcesToReload.add(changedResource);
+						}
 					} else {
 						savedResources.remove(changedResource);
 					}
+					if (internalModificationAdapters.hasNext()) {
+						changedResource.eAdapters().remove(internalModificationAdapters.next());
+					}
 				}
 
 				// Finally, we treat each removed or changed resource.
 				treatRemovedResources(removedResources);
-				treatChangedResources(changedResources);
+				treatChangedResources(changedResources, changedResourcesToReload);
 
 			} catch (CoreException e) {
 				// TODO define a standard reaction to this exception :
@@ -176,28 +190,22 @@
 	 * 
 	 * @param changedResources
 	 *            the list of the recently changed resources
+	 * @param changedResourcesToReload
+	 *            a subset of changedResources indicating resources that have been externally modified (and
+	 *            hence should be reloaded)
 	 */
-	private void treatChangedResources(Collection<Resource> changedResources) {
+	private void treatChangedResources(Collection<Resource> changedResources,
+			final Collection<Resource> changedResourcesToReload) {
 		// For each changed resources
 		for (final Resource changedResource : changedResources) {
 			if (repositoryAdapter != null) {
 				repositoryAdapter.execute(new IntentCommand() {
 
 					public void execute() {
-						// TODO [DISABLED]
-						// this make the resource unstable, some commands launched within the bad timing will
-						// have side
-						// effects
-						// we want to reload the resource if it has been modified by another tool, but not if
-						// it has been
-						// modified by a client
-						// temporary workaround: disabling
-						// System.out.println("Reloading " + changedResource + "...");
-						// // We reload this resource (if it's loaded, otherwise we simply do nothing)
-						//
-						// reloadResource(changedResource);
-						//
-						// System.out.println(changedResource + " reloaded.");
+						// Reload resource only in case of an external modification
+						if (changedResourcesToReload.contains(changedResource)) {
+							reloadResource(changedResource);
+						}
 
 						// Finally, we notify the listeners of this session
 						notifyListeners(changedResource);
@@ -214,7 +222,7 @@
 	 * @param changedResource
 	 *            the changed resource
 	 */
-	private void reloadResource(final Resource changedResource) {
+	private void reloadResource(Resource changedResource) {
 		// We get the adapters defined on the roots (in order to re-attach them after this
 		// resource will be reloaded)
 		final Collection<Adapter> oldAdaptersList = new ArrayList<Adapter>();
@@ -223,13 +231,9 @@
 		}
 		changedResource.unload();
 		try {
-			repository.getResourceSet().getResource(changedResource.getURI(), true);
-		} catch (WrappedException we) {
-			try {
-				changedResource.load(WorkspaceAdapter.getLoadOptions());
-			} catch (IOException e) {
-				// TODO Handle this I/O Exception
-			}
+			changedResource.load(WorkspaceAdapter.getLoadOptions());
+		} catch (IOException e) {
+			// TODO Handle this I/O Exception
 		}
 		// We re-attach the eAdapters to the roots
 		for (EObject root : changedResource.getContents()) {
diff --git a/tests/org.eclipse.mylyn.docs.intent.client.ui.test/src/org/eclipse/mylyn/docs/intent/client/ui/test/suite/IntentPluginTestSuite.java b/tests/org.eclipse.mylyn.docs.intent.client.ui.test/src/org/eclipse/mylyn/docs/intent/client/ui/test/suite/IntentPluginTestSuite.java
index d0cc6b9..bf7d9fd 100644
--- a/tests/org.eclipse.mylyn.docs.intent.client.ui.test/src/org/eclipse/mylyn/docs/intent/client/ui/test/suite/IntentPluginTestSuite.java
+++ b/tests/org.eclipse.mylyn.docs.intent.client.ui.test/src/org/eclipse/mylyn/docs/intent/client/ui/test/suite/IntentPluginTestSuite.java
@@ -25,6 +25,7 @@
 import org.eclipse.mylyn.docs.intent.client.ui.test.unit.refresher.RefresherTest;
 import org.eclipse.mylyn.docs.intent.client.ui.test.unit.repository.IntentURITest;
 import org.eclipse.mylyn.docs.intent.client.ui.test.unit.scenario.CompilerNotificationsTest;
+import org.eclipse.mylyn.docs.intent.client.ui.test.unit.scenario.ExternalChangesTest;
 import org.eclipse.mylyn.docs.intent.client.ui.test.unit.scenario.ExternalContentReferencesTest;
 import org.eclipse.mylyn.docs.intent.client.ui.test.unit.scenario.IntentAbstractResourceTest;
 import org.eclipse.mylyn.docs.intent.client.ui.test.unit.scenario.IntentDocumentationUpdateDoesNotCauseResolvingIssuesTest;
@@ -126,6 +127,7 @@
 		scenarioSuite.addTestSuite(ExternalContentReferencesTest.class);
 		scenarioSuite.addTestSuite(IntentHyperLinkDetetectorTest.class);
 		scenarioSuite.addTestSuite(ExternalParsersTest.class);
+		scenarioSuite.addTestSuite(ExternalChangesTest.class);
 		return scenarioSuite;
 	}
 
diff --git a/tests/org.eclipse.mylyn.docs.intent.client.ui.test/src/org/eclipse/mylyn/docs/intent/client/ui/test/unit/scenario/ExternalChangesTest.java b/tests/org.eclipse.mylyn.docs.intent.client.ui.test/src/org/eclipse/mylyn/docs/intent/client/ui/test/unit/scenario/ExternalChangesTest.java
new file mode 100644
index 0000000..d5e1771
--- /dev/null
+++ b/tests/org.eclipse.mylyn.docs.intent.client.ui.test/src/org/eclipse/mylyn/docs/intent/client/ui/test/unit/scenario/ExternalChangesTest.java
@@ -0,0 +1,232 @@
+/*******************************************************************************
+ * Copyright (c) 2013 Obeo.
+ * 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:
+ *     Obeo - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.mylyn.docs.intent.client.ui.test.unit.scenario;
+
+import java.io.IOException;
+
+import org.eclipse.core.runtime.AssertionFailedException;
+import org.eclipse.emf.common.util.URI;
+import org.eclipse.emf.ecore.EObject;
+import org.eclipse.emf.ecore.EPackage;
+import org.eclipse.emf.ecore.resource.Resource;
+import org.eclipse.emf.ecore.resource.impl.ResourceSetImpl;
+import org.eclipse.mylyn.docs.intent.client.ui.editor.IntentEditor;
+import org.eclipse.mylyn.docs.intent.client.ui.editor.IntentEditorDocument;
+import org.eclipse.mylyn.docs.intent.client.ui.test.util.AbstractIntentUITest;
+import org.eclipse.mylyn.docs.intent.collab.common.location.IntentLocations;
+import org.eclipse.mylyn.docs.intent.collab.ide.repository.WorkspaceRepository;
+import org.eclipse.mylyn.docs.intent.core.document.IntentDocument;
+import org.eclipse.mylyn.docs.intent.core.document.IntentDocumentFactory;
+import org.eclipse.mylyn.docs.intent.core.document.IntentSection;
+import org.eclipse.mylyn.docs.intent.core.modelingunit.ContributionInstruction;
+import org.eclipse.mylyn.docs.intent.core.modelingunit.ModelingUnit;
+import org.eclipse.mylyn.docs.intent.core.modelingunit.ModelingUnitFactory;
+import org.eclipse.mylyn.docs.intent.core.modelingunit.ModelingUnitInstructionReference;
+import org.eclipse.mylyn.docs.intent.core.modelingunit.NativeValue;
+import org.eclipse.mylyn.docs.intent.core.modelingunit.StructuralFeatureAffectation;
+
+/**
+ * An test ensuring that external changes (i.e. changes on the document made without using the
+ * RepositoryAdapter, for example git updates) are correctly handled.
+ * 
+ * @author <a href="mailto:alex.lagarde@obeo.fr">Alex Lagarde</a>
+ */
+public class ExternalChangesTest extends AbstractIntentUITest {
+	/**
+	 * Path to test file.
+	 */
+	private static final String INTENT_DOCUMENT_EXAMPLE_PATH = "data/unit/documents/scenario/abstract_resources.intent";
+
+	/**
+	 * The current Intent editor.
+	 */
+	private IntentEditor editor;
+
+	/**
+	 * The document associated to the current Intent editor.
+	 */
+	private IntentEditorDocument document;
+
+	/**
+	 * URI of the {@link IntentDocument} to modify externally.
+	 */
+	private URI documentURI;
+
+	/**
+	 * {@inheritDoc}
+	 * 
+	 * @see junit.framework.TestCase#setUp()
+	 */
+	@Override
+	protected void setUp() throws Exception {
+		super.setUp();
+		setUpIntentProject("intentProject", INTENT_DOCUMENT_EXAMPLE_PATH, true);
+		documentURI = URI.createURI("platform:/resource/" + intentProject.getName() + "/.repository/"
+				+ IntentLocations.INTENT_INDEX + '.' + WorkspaceRepository.getWorkspaceResourceExtension());
+	}
+
+	/**
+	 * Ensures that external changes (i.e. changes on the document made without using the RepositoryAdapter,
+	 * for example git updates) are correctly handled.
+	 */
+	public void testExternalChangesOnCompiler() {
+		// Step 1: modify the IntentDocument model without using the repository adapter
+		// Adding a new modeling unit that contribute to the abstract root by setting its nsURI
+		IntentDocument intentDocument = (IntentDocument)new ResourceSetImpl().getResource(documentURI, true)
+				.getContents().iterator().next();
+		IntentSection intentSection = intentDocument.getSubSections().get(0).getSubSections().get(0);
+		assertEquals("Wrong initial state:expecting only 2 modeling units", 2, intentSection
+				.getModelingUnits().size());
+		ModelingUnit newModelingUnit = ModelingUnitFactory.eINSTANCE.createModelingUnit();
+		ContributionInstruction contributionInstruction = ModelingUnitFactory.eINSTANCE
+				.createContributionInstruction();
+		ModelingUnitInstructionReference contributionReference = ModelingUnitFactory.eINSTANCE
+				.createModelingUnitInstructionReference();
+		contributionReference.setIntentHref("myAbstractRoot");
+		contributionInstruction.setContributionReference(contributionReference);
+		StructuralFeatureAffectation setNsURIInstruction = ModelingUnitFactory.eINSTANCE
+				.createStructuralFeatureAffectation();
+		setNsURIInstruction.setName("nsURI");
+		NativeValue nsURIValue = ModelingUnitFactory.eINSTANCE.createNativeValue();
+		nsURIValue.setValue("myNsURI");
+		setNsURIInstruction.getValues().add(nsURIValue);
+		contributionInstruction.getContributions().add(setNsURIInstruction);
+		newModelingUnit.getInstructions().add(contributionInstruction);
+		intentSection.getIntentContent().add(newModelingUnit);
+
+		repositoryListener.clearPreviousEntries();
+		try {
+			intentDocument.eResource().save(null);
+			Thread.sleep(1000);
+		} catch (IOException e) {
+			AssertionFailedException assertionFailed = new AssertionFailedException(e.getMessage());
+			assertionFailed.setStackTrace(e.getStackTrace());
+			throw assertionFailed;
+		} catch (InterruptedException e) {
+			fail(e.getMessage());
+		}
+
+		// Step 2: check that intent model was updated
+		EObject intentDocumentAsLoadedByRepositoryAdapter = repositoryAdapter
+				.getResource(IntentLocations.INTENT_INDEX).getContents().iterator().next();
+		IntentSection intentSectionAsLoadedByRepositoryAdapter = ((IntentDocument)intentDocumentAsLoadedByRepositoryAdapter)
+				.getSubSections().get(0).getSubSections().get(0);
+		assertEquals("Intent section should have been reloaded due to external changes", 3,
+				intentSectionAsLoadedByRepositoryAdapter.getModelingUnits().size());
+
+		// Step 3: check that compiler was called
+		waitForCompiler();
+		Resource generatedResource = repositoryAdapter
+				.getResource(IntentLocations.GENERATED_RESOURCES_FOLDER_PATH + "/abstractResource");
+		assertNotNull("Compiler did not re-generate the resource due to external changes", generatedResource);
+		assertTrue("Compiler generated an empty resource",
+				generatedResource.getContents().iterator().next() instanceof EPackage);
+		assertEquals("Compiler did not take in acount external changes", "myNsURI",
+				((EPackage)generatedResource.getContents().iterator().next()).getNsURI());
+
+	}
+
+	/**
+	 * Ensures that external changes (i.e. changes on the document made without using the RepositoryAdapter,
+	 * for example git updates) are correctly handled.
+	 */
+	public void testExternalChangesOnDocumentWithDocumentOpened() {
+		// Step 1 : open an editor on the root document
+		editor = openIntentEditor();
+		document = (IntentEditorDocument)editor.getDocumentProvider().getDocument(editor.getEditorInput());
+
+		// Step 2: modify the IntentDocument model without using the repository adapter
+		IntentDocument intentDocument = (IntentDocument)new ResourceSetImpl().getResource(documentURI, true)
+				.getContents().iterator().next();
+		assertEquals("Wrong initial state: Intent document should have only one chapter", 1, intentDocument
+				.eContents().size());
+		assertEquals("Wrong initial state: Intent document should have only one chapter", 2, document.get()
+				.split("Chapter").length);
+
+		intentDocument.getIntentContent().add(IntentDocumentFactory.eINSTANCE.createIntentSection());
+		try {
+			intentDocument.eResource().save(null);
+			Thread.sleep(1000);
+		} catch (IOException e) {
+			AssertionFailedException assertionFailed = new AssertionFailedException(e.getMessage());
+			assertionFailed.setStackTrace(e.getStackTrace());
+			throw assertionFailed;
+		} catch (InterruptedException e) {
+			fail(e.getMessage());
+		}
+
+		// Step 3: check that intent model was updated
+		EObject intentDocumentAsLoadedByRepositoryAdapter = repositoryAdapter
+				.getResource(IntentLocations.INTENT_INDEX).getContents().iterator().next();
+		assertEquals("Intent document should have been reloaded due to external changes", 2,
+				intentDocumentAsLoadedByRepositoryAdapter.eContents().size());
+
+		// Step 4: check that editor was updated
+		waitForAllOperationsInUIThread();
+		assertEquals("Editor should have been reloaded", 3, document.get().split("Chapter").length);
+	}
+
+	/**
+	 * Ensures that external changes (i.e. changes on the document made without using the RepositoryAdapter,
+	 * for example git updates) are correctly handled.
+	 */
+	public void testExternalChangesOnChapterWithDocumentOpened() {
+		// Step 1 : open an editor on the root document
+		editor = openIntentEditor();
+		document = (IntentEditorDocument)editor.getDocumentProvider().getDocument(editor.getEditorInput());
+
+		// Step 2: modify the IntentChapter without using the repository adapter
+		IntentDocument intentDocument = (IntentDocument)new ResourceSetImpl().getResource(documentURI, true)
+				.getContents().iterator().next();
+		IntentSection intentSection = intentDocument.getSubSections().get(0).getSubSections().get(0);
+		assertEquals("Wrong initial state: Intent section should have only 2 modeling units", 2,
+				intentSection.getModelingUnits().size());
+		assertEquals("Wrong initial state: Intent section should have only 2 modeling units", 3, document
+				.get().split("@M").length);
+
+		intentSection.getIntentContent().add(ModelingUnitFactory.eINSTANCE.createModelingUnit());
+		try {
+			intentDocument.eResource().save(null);
+			Thread.sleep(1000);
+		} catch (IOException e) {
+			AssertionFailedException assertionFailed = new AssertionFailedException(e.getMessage());
+			assertionFailed.setStackTrace(e.getStackTrace());
+			throw assertionFailed;
+		} catch (InterruptedException e) {
+			fail(e.getMessage());
+		}
+
+		// Step 3: check that intent model was updated
+		EObject intentDocumentAsLoadedByRepositoryAdapter = repositoryAdapter
+				.getResource(IntentLocations.INTENT_INDEX).getContents().iterator().next();
+		IntentSection intentSectionAsLoadedByRepositoryAdapter = ((IntentDocument)intentDocumentAsLoadedByRepositoryAdapter)
+				.getSubSections().get(0).getSubSections().get(0);
+		assertEquals("Intent document should have been reloaded due to external changes", 3,
+				intentSectionAsLoadedByRepositoryAdapter.getModelingUnits().size());
+
+		// Step 4: check that editor was updated
+		waitForAllOperationsInUIThread();
+		assertEquals("Editor should have been reloaded", 4, document.get().split("@M").length);
+	}
+
+	/**
+	 * {@inheritDoc}
+	 * 
+	 * @see junit.framework.TestCase#tearDown()
+	 */
+	@Override
+	protected void tearDown() throws Exception {
+		if (editor != null) {
+			editor.close(false);
+		}
+		super.tearDown();
+	}
+}