Added operation to update traces

The new UpdateTraceOperation updates structural features of trace links
which are identified with their name to a given value. Internally, the
operation executes a write transaction on the provided trace link. The
current implementation does not automatically update the wrapping
connection if the structural feature that is being changed is the
origins or the targets.

Change-Id: Ifc40b8aa05e9f098abe88fd8bf97e95bbcbd77ca
diff --git a/bundles/org.eclipse.capra.core/src/org/eclipse/capra/core/helpers/TraceHelper.java b/bundles/org.eclipse.capra.core/src/org/eclipse/capra/core/helpers/TraceHelper.java
index 76834c5..bbd9732 100644
--- a/bundles/org.eclipse.capra.core/src/org/eclipse/capra/core/helpers/TraceHelper.java
+++ b/bundles/org.eclipse.capra.core/src/org/eclipse/capra/core/helpers/TraceHelper.java
@@ -27,10 +27,16 @@
 import org.eclipse.capra.core.handlers.AnnotationException;
 import org.eclipse.capra.core.handlers.IAnnotateArtifact;
 import org.eclipse.capra.core.handlers.IArtifactHandler;
+import org.eclipse.emf.common.command.Command;
 import org.eclipse.emf.ecore.EClass;
 import org.eclipse.emf.ecore.EObject;
+import org.eclipse.emf.ecore.EStructuralFeature;
 import org.eclipse.emf.ecore.resource.impl.ResourceSetImpl;
 import org.eclipse.emf.ecore.util.EcoreUtil;
+import org.eclipse.emf.transaction.RecordingCommand;
+import org.eclipse.emf.transaction.RollbackException;
+import org.eclipse.emf.transaction.TransactionalCommandStack;
+import org.eclipse.emf.transaction.TransactionalEditingDomain;
 
 /**
  * Helper class for creating traces
@@ -71,6 +77,47 @@
 	}
 
 	/**
+	 * Updates the given feature of the given trace link to the given value.
+	 * <p>
+	 * The update is executed within a transaction. Rethrows a
+	 * {@link RollbackException} as an {@link IllegalStateException} to mask the
+	 * EMF-specific {@code RollbackException} from classes that use this method.
+	 * <p>
+	 * Please note that this implementation does not update the wrapping connection
+	 * in case the origins or targets are being changed.
+	 * 
+	 * @param connection  the connection whose trace link should be updated
+	 * @param featureName the feature of the trace link that should be updated
+	 * @param value       the value to which the feature should be updated
+	 * @throws InterruptedException  if the current thread is interrupted while
+	 *                               waiting to start a read/write transaction for
+	 *                               the command execution
+	 * @throws IllegalStateException if the changes performed by the command are
+	 *                               rolled back by validation of the transaction
+	 */
+	public void updateTrace(Connection connection, String featureName, Object value)
+			throws InterruptedException, IllegalStateException {
+		TransactionalEditingDomain editingDomain = EditingDomainHelper.getEditingDomain();
+		// We're saving the trace model and the artifact model in the same transaction
+		Command cmd = new RecordingCommand(editingDomain, "Update Trace Model") {
+			@Override
+			protected void doExecute() {
+				EStructuralFeature structuralFeature = connection.getTlink().eClass()
+						.getEStructuralFeature(featureName);
+				connection.getTlink().eSet(structuralFeature, value);
+			}
+		};
+
+		try {
+			((TransactionalCommandStack) editingDomain.getCommandStack()).execute(cmd, null);
+		} catch (RollbackException rbe) {
+			// We need to rethrow to not expose RollbackException to other plugins
+			throw new IllegalStateException(rbe.getMessage(), rbe.getCause());
+		}
+
+	}
+
+	/**
 	 * Annotate artifacts represented by wrappers
 	 *
 	 * @param wrappers
diff --git a/bundles/org.eclipse.capra.ui/src/org/eclipse/capra/ui/adapters/ConnectionAdapter.java b/bundles/org.eclipse.capra.ui/src/org/eclipse/capra/ui/adapters/ConnectionAdapter.java
index 4a1bbd1..9ff7e79 100644
--- a/bundles/org.eclipse.capra.ui/src/org/eclipse/capra/ui/adapters/ConnectionAdapter.java
+++ b/bundles/org.eclipse.capra.ui/src/org/eclipse/capra/ui/adapters/ConnectionAdapter.java
@@ -22,8 +22,14 @@
 import org.eclipse.capra.core.helpers.ArtifactHelper;
 import org.eclipse.capra.core.helpers.EditingDomainHelper;
 import org.eclipse.capra.core.helpers.ExtensionPointHelper;
+import org.eclipse.capra.ui.operations.UpdateTraceOperation;
+import org.eclipse.core.commands.ExecutionException;
+import org.eclipse.core.commands.operations.IOperationHistory;
+import org.eclipse.core.commands.operations.OperationHistoryFactory;
+import org.eclipse.core.runtime.IAdaptable;
 import org.eclipse.emf.ecore.EObject;
 import org.eclipse.emf.ecore.EStructuralFeature;
+import org.eclipse.ui.PlatformUI;
 import org.eclipse.ui.views.properties.IPropertyDescriptor;
 import org.eclipse.ui.views.properties.IPropertySource;
 import org.eclipse.ui.views.properties.PropertyDescriptor;
@@ -117,8 +123,17 @@
 
 	@Override
 	public void setPropertyValue(Object id, Object value) {
-		EStructuralFeature feature = connection.getTlink().eClass().getEStructuralFeature((String) id);
-		connection.getTlink().eSet(feature, value);
+		IAdaptable adapter = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage().getActivePart()
+				.getSite();
+		IOperationHistory operationHistory = OperationHistoryFactory.getOperationHistory();
+		UpdateTraceOperation updateTraceOperation = new UpdateTraceOperation("Update trace link", this.connection, id,
+				value);
+		updateTraceOperation.addContext(IOperationHistory.GLOBAL_UNDO_CONTEXT);
+		try {
+			operationHistory.execute(updateTraceOperation, null, adapter);
+		} catch (ExecutionException e) {
+			// Deliberately do nothing. Exceptions are caught by the operation.
+		}
 	}
 
 	/**
diff --git a/bundles/org.eclipse.capra.ui/src/org/eclipse/capra/ui/operations/CreateTraceOperation.java b/bundles/org.eclipse.capra.ui/src/org/eclipse/capra/ui/operations/CreateTraceOperation.java
index f2083fa..4acd67a 100644
--- a/bundles/org.eclipse.capra.ui/src/org/eclipse/capra/ui/operations/CreateTraceOperation.java
+++ b/bundles/org.eclipse.capra.ui/src/org/eclipse/capra/ui/operations/CreateTraceOperation.java
@@ -42,7 +42,6 @@
 import org.eclipse.emf.ecore.EClass;
 import org.eclipse.emf.ecore.EObject;
 import org.eclipse.emf.ecore.resource.ResourceSet;
-import org.eclipse.jface.dialogs.ErrorDialog;
 import org.eclipse.jface.dialogs.MessageDialog;
 import org.eclipse.jface.dialogs.MessageDialogWithToggle;
 import org.eclipse.jface.preference.IPreferenceStore;
@@ -72,7 +71,6 @@
 	private static final String SELECT_TRACE_LINK_TYPE = "Select the trace type you want to create";
 	private static final String SOURCE = "Source:";
 	private static final String TARGET = "Target:";
-	private static final String PLUGIN_ID = "org.eclipse.capra.ui";
 
 	private Optional<EClass> chosenType;
 	private List<EObject> originWrappers;
@@ -116,17 +114,19 @@
 			createTrace(shell, chooseTraceType);
 			executionStatus = Status.OK_STATUS;
 		} catch (IllegalStateException e) {
-			executionStatus = new Status(Status.ERROR, PLUGIN_ID, e.getMessage(), e);
-			createErrorMessage(shell, executionStatus, e.getMessage());
+			executionStatus = new Status(Status.ERROR, OperationsHelper.PLUGIN_ID, e.getMessage(), e);
+			OperationsHelper.createErrorMessage(shell, executionStatus, ERROR_DIALOG_TITLE, e.getMessage());
 		} catch (ClassCastException e) {
-			executionStatus = new Status(Status.ERROR, PLUGIN_ID,
+			executionStatus = new Status(Status.ERROR, OperationsHelper.PLUGIN_ID,
 					String.format(EXCEPTION_MESSAGE_CLASS_CAST_EXCEPTION_REASON, returnElementClasses(this.origins),
 							returnElementClasses(this.targets)),
 					e);
-			createErrorMessage(shell, executionStatus, EXCEPTION_MESSAGE_CLASS_CAST_EXCEPTION);
+			OperationsHelper.createErrorMessage(shell, executionStatus, ERROR_DIALOG_TITLE,
+					EXCEPTION_MESSAGE_CLASS_CAST_EXCEPTION);
 		} catch (RuntimeException e) {
-			executionStatus = new Status(Status.ERROR, PLUGIN_ID, e.getMessage(), e);
-			createErrorMessage(shell, executionStatus, EXCEPTION_MESSAGE_RUNTIME_EXCEPTION);
+			executionStatus = new Status(Status.ERROR, OperationsHelper.PLUGIN_ID, e.getMessage(), e);
+			OperationsHelper.createErrorMessage(shell, executionStatus, ERROR_DIALOG_TITLE,
+					EXCEPTION_MESSAGE_RUNTIME_EXCEPTION);
 		}
 		return executionStatus;
 
@@ -257,10 +257,6 @@
 
 	}
 
-	private static void createErrorMessage(Shell shell, IStatus status, String message) {
-		ErrorDialog.openError(shell, ERROR_DIALOG_TITLE, message, status);
-	}
-
 	private static String returnElementClasses(List<?> elements) {
 		Set<String> unique = new HashSet<String>();
 		elements.forEach(el -> unique.add(el.getClass().getCanonicalName()));
diff --git a/bundles/org.eclipse.capra.ui/src/org/eclipse/capra/ui/operations/OperationsHelper.java b/bundles/org.eclipse.capra.ui/src/org/eclipse/capra/ui/operations/OperationsHelper.java
new file mode 100644
index 0000000..fd4952c
--- /dev/null
+++ b/bundles/org.eclipse.capra.ui/src/org/eclipse/capra/ui/operations/OperationsHelper.java
@@ -0,0 +1,47 @@
+/*******************************************************************************
+ * Copyright (c) 2016-2021 Chalmers | University of Gothenburg, rt-labs and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v2.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v20.html
+ *  
+ * SPDX-License-Identifier: EPL-2.0
+ *  
+ * Contributors:
+ *      Chalmers | University of Gothenburg and rt-labs - initial API and implementation and/or initial documentation
+ *      Chalmers | University of Gothenburg - additional features, updated API
+ *******************************************************************************/
+package org.eclipse.capra.ui.operations;
+
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.jface.dialogs.ErrorDialog;
+import org.eclipse.swt.widgets.Shell;
+
+/**
+ * Provides common elements for operations used by Eclipse Capra.
+ * 
+ * @author Jan-Philipp Steghöfer
+ */
+public class OperationsHelper {
+
+	/**
+	 * The plugin ID.
+	 */
+	public static final String PLUGIN_ID = "org.eclipse.capra.ui";
+
+	/**
+	 * Creates an error dialog that exposes an error to the user.
+	 * 
+	 * @param shell   the parent shell of the dialog
+	 * @param status  the error to show to the user
+	 * @param the     title to use for this dialog, or <code>null</code> to indicate
+	 *                that the default title should be used
+	 * @param message the message to show in this dialog, or <code>null</code> to
+	 *                indicate that the error's message should be shown as the
+	 *                primary message
+	 */
+	public static void createErrorMessage(Shell shell, IStatus status, String title, String message) {
+		ErrorDialog.openError(shell, title, message, status);
+	}
+
+}
diff --git a/bundles/org.eclipse.capra.ui/src/org/eclipse/capra/ui/operations/UpdateTraceOperation.java b/bundles/org.eclipse.capra.ui/src/org/eclipse/capra/ui/operations/UpdateTraceOperation.java
new file mode 100644
index 0000000..a305efe
--- /dev/null
+++ b/bundles/org.eclipse.capra.ui/src/org/eclipse/capra/ui/operations/UpdateTraceOperation.java
@@ -0,0 +1,158 @@
+/*******************************************************************************
+ * Copyright (c) 2016-2021 Chalmers | University of Gothenburg, rt-labs and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v2.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v20.html
+ *  
+ * SPDX-License-Identifier: EPL-2.0
+ *  
+ * Contributors:
+ *      Chalmers | University of Gothenburg and rt-labs - initial API and implementation and/or initial documentation
+ *      Chalmers | University of Gothenburg - additional features, updated API
+ *******************************************************************************/
+package org.eclipse.capra.ui.operations;
+
+import org.eclipse.capra.core.adapters.Connection;
+import org.eclipse.capra.core.adapters.TracePersistenceAdapter;
+import org.eclipse.capra.core.helpers.EditingDomainHelper;
+import org.eclipse.capra.core.helpers.ExtensionPointHelper;
+import org.eclipse.capra.core.helpers.TraceHelper;
+import org.eclipse.core.commands.ExecutionException;
+import org.eclipse.core.commands.operations.AbstractOperation;
+import org.eclipse.core.runtime.IAdaptable;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.emf.ecore.EObject;
+import org.eclipse.emf.ecore.EStructuralFeature;
+import org.eclipse.emf.ecore.resource.ResourceSet;
+import org.eclipse.swt.widgets.Shell;
+
+/**
+ * Operation to update trace links. Also allows to undo and redo the operation.
+ * 
+ * @author Jan-Philipp Steghöfer
+ *
+ */
+public class UpdateTraceOperation extends AbstractOperation {
+
+	private static final String ERROR_DIALOG_TITLE = "Error updating trace link";
+	private static final String EXCEPTION_MESSAGE_RUNTIME_EXCEPTION = "An exception occured during the update of the trace link.";
+	private static final String EXCEPTION_MESSAGE_INTERRUPTED_EXCEPTION = "The transaction to update the trace link was interrupted.";
+
+	private Connection connection;
+	private String featureName;
+	private Object postUpdateValue;
+	private Object preUpdateValue;
+
+	/**
+	 * Creates a new operation to update trace links.
+	 * 
+	 * @param label      the label used for the operation. Should never be
+	 *                   <code>null</code>.
+	 * @param connection the connection representing the trace link that should be
+	 *                   updated.
+	 * @param feature    the feature that should be updated. This value will be cast
+	 *                   to <code>String</code> and an
+	 *                   {@link IllegalArgumentException} will be thrown if it is
+	 *                   not a string.
+	 * @param value      the value to which the feature should be updated.
+	 */
+	public UpdateTraceOperation(String label, Connection connection, Object feature, Object value) {
+		super(label);
+		this.connection = connection;
+		if (!(feature instanceof String)) {
+			throw new IllegalArgumentException("Feature must be a string!");
+		}
+		this.featureName = (String) feature;
+		this.postUpdateValue = value;
+		this.preUpdateValue = getValue(connection, featureName);
+	}
+
+	@Override
+	public IStatus execute(IProgressMonitor monitor, IAdaptable info) throws ExecutionException {
+		return executeTraceUpdate(info, postUpdateValue);
+	}
+
+	@Override
+	public IStatus redo(IProgressMonitor monitor, IAdaptable info) throws ExecutionException {
+		return execute(monitor, info);
+	}
+
+	@Override
+	public IStatus undo(IProgressMonitor monitor, IAdaptable info) throws ExecutionException {
+		return executeTraceUpdate(info, preUpdateValue);
+	}
+
+	/**
+	 * Executes the update of the trace link.
+	 * 
+	 * @param info  the {@code IAdaptable} which called this operation (should
+	 *              provide a {@code Shell} adapter
+	 * @param value the value to set the feature to
+	 * @return the status of the update: {@link Status.OK_STATUS} if everything
+	 *         worked as expected, {@link Status.ERROR} otherwise
+	 */
+	private IStatus executeTraceUpdate(IAdaptable info, Object value) {
+		Shell shell = info.getAdapter(Shell.class);
+		IStatus executionStatus = null;
+		try {
+			updateTrace(connection, featureName, value);
+			executionStatus = Status.OK_STATUS;
+		} catch (IllegalStateException e) {
+			executionStatus = new Status(Status.ERROR, OperationsHelper.PLUGIN_ID, e.getMessage(), e);
+			OperationsHelper.createErrorMessage(shell, executionStatus, ERROR_DIALOG_TITLE, e.getMessage());
+		} catch (InterruptedException e) {
+			executionStatus = new Status(Status.ERROR, OperationsHelper.PLUGIN_ID, e.getMessage(), e);
+			OperationsHelper.createErrorMessage(shell, executionStatus, ERROR_DIALOG_TITLE,
+					EXCEPTION_MESSAGE_INTERRUPTED_EXCEPTION);
+		} catch (RuntimeException e) {
+			executionStatus = new Status(Status.ERROR, OperationsHelper.PLUGIN_ID, e.getMessage(), e);
+			OperationsHelper.createErrorMessage(shell, executionStatus, ERROR_DIALOG_TITLE,
+					EXCEPTION_MESSAGE_RUNTIME_EXCEPTION);
+		}
+		return executionStatus;
+	}
+
+	/**
+	 * Retrieves the value of a structural features of the trace link encapsulated
+	 * in the given {@link Connection}.
+	 * 
+	 * @param connection  the {@code Connection} that contains the trace link whose
+	 *                    structural feature should be retrieved.
+	 * @param featureName the name of the structural feature to retrieve
+	 * @return the value of the structural feature
+	 */
+	private Object getValue(Connection connection, String featureName) {
+		EStructuralFeature structuralFeature = connection.getTlink().eClass().getEStructuralFeature(featureName);
+		return connection.getTlink().eGet(structuralFeature);
+	}
+
+	/**
+	 * Updates the trace link in the provided {@link Connection} by setting the
+	 * given feature to the given value.
+	 * 
+	 * @param connection  the connection that wraps the trace link that should be
+	 *                    updated
+	 * @param featureName the name of the structural feature to update
+	 * @param value       the value to set the structural feature to
+	 * @throws IllegalStateException if the transaction in which the trace is
+	 *                               updated is rolled back
+	 * @throws InterruptedException  if the transaction in which the trace is
+	 *                               updated is interrupted
+	 */
+	private void updateTrace(Connection connection, String featureName, Object value)
+			throws IllegalStateException, InterruptedException {
+		TracePersistenceAdapter persistenceAdapter = ExtensionPointHelper.getTracePersistenceAdapter().orElseThrow();
+
+		ResourceSet resourceSet = EditingDomainHelper.getResourceSet();
+		EObject traceModel = persistenceAdapter.getTraceModel(resourceSet);
+		TraceHelper traceHelper = new TraceHelper(traceModel);
+		EObject artifactModel = persistenceAdapter.getArtifactWrappers(resourceSet);
+
+		traceHelper.updateTrace(connection, featureName, value);
+		persistenceAdapter.saveTracesAndArtifacts(traceModel, artifactModel);
+	}
+
+}
diff --git a/tests/org.eclipse.capra.testsuite/src/org/eclipse/capra/testsuite/TestUpdateTraceOperation.java b/tests/org.eclipse.capra.testsuite/src/org/eclipse/capra/testsuite/TestUpdateTraceOperation.java
new file mode 100644
index 0000000..eb2e2d0
--- /dev/null
+++ b/tests/org.eclipse.capra.testsuite/src/org/eclipse/capra/testsuite/TestUpdateTraceOperation.java
@@ -0,0 +1,184 @@
+/*******************************************************************************
+ * Copyright (c) 2016, 2021 Chalmers | University of Gothenburg, rt-labs and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v2.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v20.html
+ *  
+ * SPDX-License-Identifier: EPL-2.0
+ *  
+ * Contributors:
+ *      Chalmers | University of Gothenburg and rt-labs - initial API and implementation and/or initial documentation
+ *      Chalmers | University of Gothenburg - additional features, updated API
+ *******************************************************************************/
+package org.eclipse.capra.testsuite;
+
+import static org.eclipse.capra.testsupport.TestHelper.clearWorkspace;
+import static org.eclipse.capra.testsupport.TestHelper.createEClassInEPackage;
+import static org.eclipse.capra.testsupport.TestHelper.createEcoreModel;
+import static org.eclipse.capra.testsupport.TestHelper.createSimpleProject;
+import static org.eclipse.capra.testsupport.TestHelper.getProject;
+import static org.eclipse.capra.testsupport.TestHelper.load;
+import static org.eclipse.capra.testsupport.TestHelper.projectExists;
+import static org.eclipse.capra.testsupport.TestHelper.purgeModels;
+import static org.eclipse.capra.testsupport.TestHelper.resetSelectionView;
+import static org.eclipse.capra.testsupport.TestHelper.save;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.List;
+
+import org.eclipse.capra.core.adapters.Connection;
+import org.eclipse.capra.core.adapters.TracePersistenceAdapter;
+import org.eclipse.capra.core.helpers.ArtifactHelper;
+import org.eclipse.capra.core.helpers.EditingDomainHelper;
+import org.eclipse.capra.core.helpers.ExtensionPointHelper;
+import org.eclipse.capra.core.helpers.TraceHelper;
+import org.eclipse.capra.generic.tracemodel.RelatedTo;
+import org.eclipse.capra.generic.tracemodel.TracemodelPackage;
+import org.eclipse.capra.testsupport.TestHelper;
+import org.eclipse.capra.ui.operations.CreateTraceOperation;
+import org.eclipse.capra.ui.operations.UpdateTraceOperation;
+import org.eclipse.capra.ui.views.SelectionView;
+import org.eclipse.core.commands.ExecutionException;
+import org.eclipse.core.commands.operations.IOperationHistory;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IAdaptable;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.emf.ecore.EClass;
+import org.eclipse.emf.ecore.EObject;
+import org.eclipse.emf.ecore.EPackage;
+import org.eclipse.emf.ecore.EStructuralFeature;
+import org.eclipse.emf.ecore.resource.ResourceSet;
+import org.eclipse.emf.ecore.resource.impl.ResourceSetImpl;
+import org.eclipse.ui.IWorkbench;
+import org.eclipse.ui.PlatformUI;
+import org.junit.Before;
+import org.junit.Test;
+
+public class TestUpdateTraceOperation {
+
+	private static final String TEST_NAME_UPDATED = "test-name-updated";
+	private static final String CLASS_A_NAME = "A";
+	private static final String CLASS_B_NAME = "B";
+	private static final String CLASS_C_NAME = "C";
+
+	private static final String MODEL_A_FILENAME = "modelA.ecore";
+	private static final String MODEL_B_FILENAME = "modelB.ecore";
+	private static final String MODEL_A_NAME = "modelA";
+	private static final String MODEL_B_NAME = "modelB";
+
+	private static final String TEST_PROJECT_NAME = "TestProject";
+
+	@Before
+	public void init() throws CoreException {
+		clearWorkspace();
+		resetSelectionView();
+		purgeModels();
+	}
+
+	/**
+	 * Tests the creation of a trace link and its subsequent update using the
+	 * operations defined in {@code org.eclipse.capra.ui.operations}.
+	 * 
+	 * @throws CoreException
+	 * @throws IOException
+	 */
+	@Test
+	public void testUpdateTrace() throws CoreException, IOException {
+		// Create a project
+		createSimpleProject(TEST_PROJECT_NAME);
+		assertTrue(projectExists(TEST_PROJECT_NAME));
+
+		// Create two models and persist them
+		IProject testProject = getProject(TEST_PROJECT_NAME);
+		EPackage a = TestHelper.createEcoreModel(MODEL_A_NAME);
+		createEClassInEPackage(a, CLASS_A_NAME);
+		save(testProject, a);
+
+		EPackage b = createEcoreModel(MODEL_B_NAME);
+		createEClassInEPackage(b, CLASS_B_NAME);
+		createEClassInEPackage(b, CLASS_C_NAME);
+		save(testProject, b);
+
+		// Load them, choose two elements
+		ResourceSet rs = new ResourceSetImpl();
+
+		EPackage _a = load(testProject, MODEL_A_FILENAME, rs);
+		assertEquals(_a.getName(), MODEL_A_NAME);
+		EClass _A = (EClass) _a.getEClassifier(CLASS_A_NAME);
+
+		EPackage _b = load(testProject, MODEL_B_FILENAME, rs);
+		assertEquals(_b.getName(), MODEL_B_NAME);
+		EClass _B = (EClass) _b.getEClassifier(CLASS_B_NAME);
+		EClass _C = (EClass) _b.getEClassifier(CLASS_C_NAME);
+
+		// Add them to the selection view
+		SelectionView.getOpenedView().dropToSelection(_A);
+		SelectionView.getOpenedView().dropToSelection(_B);
+
+		EClass traceType = TracemodelPackage.eINSTANCE.getRelatedTo();
+
+		IAdaptable adapter = SelectionView.getOpenedView().getSite();
+
+		IWorkbench workbench = PlatformUI.getWorkbench();
+		IOperationHistory operationHistory = workbench.getOperationSupport().getOperationHistory();
+
+		CreateTraceOperation createTraceOperation = TestHelper
+				.prepareCreateTraceOperationForCurrentSelectionOfType(traceType);
+		try {
+			assertEquals(operationHistory.execute(createTraceOperation, null, adapter), Status.OK_STATUS);
+		} catch (ExecutionException e) {
+			fail("Could not create trace: ExecutionException in operation");
+		}
+
+		TracePersistenceAdapter persistenceAdapter = ExtensionPointHelper.getTracePersistenceAdapter().get();
+		TraceHelper traceHelper = new TraceHelper(
+				persistenceAdapter.getTraceModel(EditingDomainHelper.getResourceSet()));
+		EObject artifactModel = persistenceAdapter.getArtifactWrappers(_A.eResource().getResourceSet());
+		ArtifactHelper artifactHelper = new ArtifactHelper(artifactModel);
+		List<EObject> selection = artifactHelper.createWrappers(SelectionView.getOpenedView().getSelection());
+
+		// Check that the trace between A and B exists
+		traceHelper = new TraceHelper(persistenceAdapter.getTraceModel(EditingDomainHelper.getResourceSet()));
+		selection = artifactHelper.createWrappers(SelectionView.getOpenedView().getSelection());
+		assertTrue(traceHelper.traceExists(selection, traceType));
+
+		List<Connection> traces = traceHelper.getTraces(Arrays.asList(new EClass[] { _A, _B }));
+		assertFalse(traces.isEmpty());
+		RelatedTo relatedTo = (RelatedTo) traces.get(0).getTlink();
+
+		EStructuralFeature structuralFeature = relatedTo.eClass().getEStructuralFeature("name");
+		Object originalName = relatedTo.eGet(structuralFeature);
+
+		UpdateTraceOperation updateOp = new UpdateTraceOperation("Update trace", traces.get(0), "name",
+				TEST_NAME_UPDATED);
+		try {
+			assertEquals(operationHistory.execute(updateOp, null, adapter), Status.OK_STATUS);
+		} catch (ExecutionException e) {
+			fail("Could not update trace: ExecutionException in operation");
+		}
+
+		Object updatedName = relatedTo.eGet(structuralFeature);
+		assertNotEquals(originalName, updatedName);
+		assertEquals(updatedName, TEST_NAME_UPDATED);
+
+		try {
+			assertEquals(operationHistory.undoOperation(updateOp, null, adapter), Status.OK_STATUS);
+		} catch (ExecutionException e) {
+			fail("Could not undo trace update: ExecutionException in operation");
+		}
+
+		Object nameAfterUndo = relatedTo.eGet(structuralFeature);
+		assertNotEquals(nameAfterUndo, TEST_NAME_UPDATED);
+		assertEquals(originalName, nameAfterUndo);
+
+	}
+
+}