[476019] Support synchronization of local user preferences with a remote service

https://bugs.eclipse.org/bugs/show_bug.cgi?id=476019
diff --git a/plugins/org.eclipse.oomph.setup.sync.tests/src/org/eclipse/oomph/setup/sync/tests/SynchronizerTests.java b/plugins/org.eclipse.oomph.setup.sync.tests/src/org/eclipse/oomph/setup/sync/tests/SynchronizerTests.java
index afc7189..730b3ce 100644
--- a/plugins/org.eclipse.oomph.setup.sync.tests/src/org/eclipse/oomph/setup/sync/tests/SynchronizerTests.java
+++ b/plugins/org.eclipse.oomph.setup.sync.tests/src/org/eclipse/oomph/setup/sync/tests/SynchronizerTests.java
@@ -12,7 +12,6 @@
 
 import org.eclipse.oomph.setup.internal.sync.DataProvider.NotCurrentException;
 import org.eclipse.oomph.setup.internal.sync.Synchronization.ConflictException;
-import org.eclipse.oomph.setup.sync.SyncActionType;
 import org.eclipse.oomph.setup.sync.tests.TestWorkstation.FailureHandler;
 import org.eclipse.oomph.setup.sync.tests.TestWorkstation.FailureHandler.Expect;
 import org.eclipse.oomph.setup.sync.tests.TestWorkstation.TestSynchronization;
@@ -38,7 +37,7 @@
     TestWorkstation workstation = workstations.get(id);
     if (workstation == null)
     {
-      workstation = new TestWorkstation(id);
+      workstation = new TestWorkstation(workstations, id);
       workstations.put(id, workstation);
     }
 
@@ -55,7 +54,7 @@
   @Test
   public void test000() throws Exception
   {
-    WS(1).synchronize().commitAnd().assertCount(0);
+    WS(1).commit().assertCount(0);
   }
 
   @Test
@@ -103,27 +102,27 @@
   @Test
   public void test006_SameKey_SameValue() throws Exception
   {
-    TestSynchronization sync1 = WS(1).set("line.numbers", "true").save().synchronize();
+    TestSynchronization sync1 = WS(1).set("property", "value").save().synchronize();
 
-    WS(2).set("line.numbers", "true").save().commit().assertCount(1).assertSet("line.numbers", "true");
+    WS(2).set("property", "value").save().commit().assertCount(1).assertSet("property", "value");
 
     sync1.commitFail(new FailureHandler()
     {
       public void handleFailure(Exception t) throws Exception
       {
-        WS(1).commit().assertCount(1).assertSet("line.numbers", "true");
+        WS(1).commit().assertCount(1).assertSet("property", "value");
       }
     });
 
-    WS(2).commit().assertCount(1).assertSet("line.numbers", "true");
+    WS(2).commit().assertCount(1).assertSet("property", "value");
   }
 
   @Test
   public void test007_SameKey_ConflictException() throws Exception
   {
-    TestSynchronization sync1 = WS(1).set("line.numbers", "true").save().synchronize();
+    TestSynchronization sync1 = WS(1).set("property", "value1").save().synchronize();
 
-    WS(2).set("line.numbers", "false").save().commit().assertCount(1).assertSet("line.numbers", "false");
+    WS(2).set("property", "value2").save().commit().assertCount(1).assertSet("property", "value2");
 
     sync1.commitFail(new Expect(NotCurrentException.class)
     {
@@ -138,24 +137,78 @@
   @Test
   public void test008_SameKey_ConflictPick1() throws Exception
   {
-    WS(1).set("line.numbers", "true").save();
+    WS(1).set("property", "value1").save();
 
-    WS(2).set("line.numbers", "false").save().commit().assertCount(1).assertSet("line.numbers", "false");
+    WS(2).set("property", "value2").save().commit().assertCount(1).assertSet("property", "value2");
 
-    WS(1).synchronize().resolvePreference("line.numbers", SyncActionType.SET_LOCAL).commitAnd().assertCount(1).assertSet("line.numbers", "true");
+    WS(1).synchronize().pickLocal("property").commitAnd().assertCount(1).assertSet("property", "value1");
 
-    WS(2).commit().assertCount(1).assertSet("line.numbers", "true");
+    WS(2).commit().assertCount(1).assertSet("property", "value1");
   }
 
   @Test
   public void test009_SameKey_ConflictPick2() throws Exception
   {
-    WS(1).set("line.numbers", "true").save();
+    WS(1).set("property", "value1").save();
 
-    WS(2).set("line.numbers", "false").save().commit().assertCount(1).assertSet("line.numbers", "false");
+    WS(2).set("property", "value2").save().commit().assertCount(1).assertSet("property", "value2");
 
-    WS(1).synchronize().resolvePreference("line.numbers", SyncActionType.SET_REMOTE).commitAnd().assertCount(1).assertSet("line.numbers", "false");
+    WS(1).synchronize().pickRemote("property").commitAnd().assertCount(1).assertSet("property", "value2");
 
-    WS(2).commit().assertCount(1).assertSet("line.numbers", "false");
+    WS(2).commit().assertCount(1).assertSet("property", "value2");
+  }
+
+  @Test
+  public void test010_Remove1_Sync1() throws Exception
+  {
+    WS(1).set("line.numbers", "true").set("refresh.resources", "true").save().commit() //
+        .assertCount(2).assertSet("line.numbers", "true").assertSet("refresh.resources", "true");
+
+    WS(1).remove("line.numbers").save().commit().assertCount(1).assertSet("refresh.resources", "true");
+    WS(1).remove("refresh.resources").save().commit().assertCount(0);
+  }
+
+  @Test
+  public void test011_Remove1_Sync1_Sync2() throws Exception
+  {
+    WS(1).set("line.numbers", "true").set("refresh.resources", "true").save().commit();
+
+    WS(2).commit().assertCount(2).assertSet("line.numbers", "true").assertSet("refresh.resources", "true");
+
+    WS(1).remove("line.numbers").save().commit().assertCount(1).assertSet("refresh.resources", "true");
+
+    WS(2).commit().assertCount(1).assertSet("refresh.resources", "true");
+
+    WS(1).remove("refresh.resources").save().commit().assertCount(0);
+
+    WS(2).commit().assertCount(0);
+  }
+
+  @Test
+  public void test012_Remove1_Remove2_Sync1_Sync2() throws Exception
+  {
+    WS(1).set("line.numbers", "true").set("refresh.resources", "true").save().commit();
+
+    WS(2).commit().assertCount(2).assertSet("line.numbers", "true").assertSet("refresh.resources", "true");
+
+    WS(1).remove("line.numbers").save().commit().assertCount(1).assertSet("refresh.resources", "true");
+
+    WS(2).remove("line.numbers").save().commit().assertCount(1).assertSet("refresh.resources", "true");
+  }
+
+  @Test
+  public void test013_Exclude() throws Exception
+  {
+    WS(1).set("line.numbers", "true").save().commit();
+
+    WS(2).commit().assertCount(1);
+    WS(2).set("refresh.resources", "true").save().synchronize().exclude("refresh.resources").commitAnd() //
+        .assertCount(2).assertSet("line.numbers", "true").assertSet("refresh.resources", "true") //
+        .assertExcluded("refresh.resources");
+
+    WS(1).commit().assertCount(1).assertSet("line.numbers", "true").assertExcluded("refresh.resources");
+    WS(1).set("refresh.resources", "true").save().commit() //
+        .assertCount(2).assertSet("line.numbers", "true").assertSet("refresh.resources", "true") //
+        .assertExcluded("refresh.resources");
   }
 }
diff --git a/plugins/org.eclipse.oomph.setup.sync.tests/src/org/eclipse/oomph/setup/sync/tests/TestWorkstation.java b/plugins/org.eclipse.oomph.setup.sync.tests/src/org/eclipse/oomph/setup/sync/tests/TestWorkstation.java
index 507fa57..f949dab 100644
--- a/plugins/org.eclipse.oomph.setup.sync.tests/src/org/eclipse/oomph/setup/sync/tests/TestWorkstation.java
+++ b/plugins/org.eclipse.oomph.setup.sync.tests/src/org/eclipse/oomph/setup/sync/tests/TestWorkstation.java
@@ -28,14 +28,17 @@
 import org.eclipse.oomph.setup.internal.sync.SyncUtil;
 import org.eclipse.oomph.setup.internal.sync.Synchronization;
 import org.eclipse.oomph.setup.internal.sync.Synchronizer;
+import org.eclipse.oomph.setup.sync.RemoteData;
 import org.eclipse.oomph.setup.sync.SyncAction;
 import org.eclipse.oomph.setup.sync.SyncActionType;
 import org.eclipse.oomph.setup.sync.SyncDelta;
+import org.eclipse.oomph.setup.sync.SyncPolicy;
 import org.eclipse.oomph.tests.AbstractTest;
 import org.eclipse.oomph.util.IOUtil;
 import org.eclipse.oomph.util.PropertiesUtil;
 
 import org.eclipse.emf.common.util.EList;
+import org.eclipse.emf.common.util.EMap;
 import org.eclipse.emf.common.util.URI;
 import org.eclipse.emf.ecore.EClassifier;
 import org.eclipse.emf.ecore.EObject;
@@ -50,6 +53,7 @@
 import java.io.File;
 import java.io.IOException;
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
@@ -64,26 +68,33 @@
 
   private final ResourceSet resourceSet = SyncUtil.createResourceSet();
 
+  private final Map<Integer, TestWorkstation> workstations;
+
   private final int id;
 
   private final File userHome;
 
   private final File userSetup;
 
-  private User user;
-
   private final TestSynchronizer synchronizer;
 
-  public TestWorkstation(int id) throws Exception
+  private File remoteFile;
+
+  private User user;
+
+  public TestWorkstation(Map<Integer, TestWorkstation> workstations, int id) throws Exception
   {
     this.id = id;
+    this.workstations = workstations;
+
     userHome = createUserHome();
     userSetup = new File(userHome, "user.setup");
-    log("Create workstation " + userHome);
 
     DataProvider localDataProvider = new LocalDataProvider(userSetup);
     DataProvider remoteDataProvider = TestServer.getRemoteDataProvider();
     synchronizer = new TestSynchronizer(localDataProvider, remoteDataProvider, userHome);
+
+    log("Create workstation " + userHome);
   }
 
   public File getUserHome()
@@ -152,19 +163,16 @@
 
   public TestWorkstation remove(String key)
   {
-    if (user != null)
+    log("Remove " + key);
+    for (SetupTask task : getUser().getSetupTasks())
     {
-      log("Remove " + key);
-      for (SetupTask task : user.getSetupTasks())
+      if (task instanceof PreferenceTask)
       {
-        if (task instanceof PreferenceTask)
+        PreferenceTask preferenceTask = (PreferenceTask)task;
+        if (key.equals(preferenceTask.getKey()))
         {
-          PreferenceTask preferenceTask = (PreferenceTask)task;
-          if (key.equals(preferenceTask.getKey()))
-          {
-            EcoreUtil.remove(preferenceTask);
-            return this;
-          }
+          EcoreUtil.remove(preferenceTask);
+          return this;
         }
       }
     }
@@ -216,6 +224,80 @@
     return this;
   }
 
+  public TestWorkstation assertIncluded(String key) throws IOException
+  {
+    Map<String, SyncPolicy> preferencePolicies = getPreferencePolicies();
+    assertThat(preferencePolicies.get(key), CoreMatchers.is(SyncPolicy.INCLUDE));
+    return this;
+  }
+
+  public TestWorkstation assertExcluded(String key) throws IOException
+  {
+    Map<String, SyncPolicy> preferencePolicies = getPreferencePolicies();
+    assertThat(preferencePolicies.get(key), CoreMatchers.is(SyncPolicy.EXCLUDE));
+    return this;
+  }
+
+  public TestWorkstation assertNoPolicy(String key) throws IOException
+  {
+    Map<String, SyncPolicy> preferencePolicies = getPreferencePolicies();
+    assertThat(preferencePolicies.get(key), IsNull.nullValue());
+    return this;
+  }
+
+  public Map<String, SyncPolicy> getPreferencePolicies() throws IOException
+  {
+    RemoteData remoteData = getRemoteData();
+    EMap<String, SyncPolicy> policies = remoteData.getPolicies();
+
+    Map<String, SyncPolicy> preferencePolicies = new HashMap<String, SyncPolicy>();
+
+    // Make sure that even policies of remotely removed tasks can be found by preference key.
+    for (TestWorkstation workstation : workstations.values())
+    {
+      if (workstation != this)
+      {
+        collectPreferencePolicies(preferencePolicies, policies, workstation.getPreferenceTasks());
+      }
+    }
+
+    collectPreferencePolicies(preferencePolicies, policies, getPreferenceTasks());
+    collectPreferencePolicies(preferencePolicies, policies, remoteData.getSetupTasks());
+    return preferencePolicies;
+  }
+
+  private void collectPreferencePolicies(Map<String, SyncPolicy> preferencePolicies, EMap<String, SyncPolicy> policies, List<? extends SetupTask> tasks)
+  {
+    for (SetupTask task : tasks)
+    {
+      if (task instanceof PreferenceTask)
+      {
+        PreferenceTask preferenceTask = (PreferenceTask)task;
+        String id = preferenceTask.getID();
+        SyncPolicy policy = policies.get(id);
+        if (policy != null)
+        {
+          String key = preferenceTask.getKey();
+          preferencePolicies.put(key, policy);
+        }
+      }
+    }
+  }
+
+  public RemoteData getRemoteData() throws IOException
+  {
+    if (remoteFile == null)
+    {
+      remoteFile = File.createTempFile("remote-data-", ".tmp");
+      remoteFile.deleteOnExit();
+
+      DataProvider dataProvider = synchronizer.getRemoteSnapshot().getDataProvider();
+      dataProvider.update(remoteFile);
+    }
+
+    return loadObject(URI.createFileURI(remoteFile.getAbsolutePath()), Synchronization.REMOTE_DATA_TYPE);
+  }
+
   public TestWorkstation log(Object msg)
   {
     if (this != lastWorkstation)
@@ -358,6 +440,14 @@
         {
           log(t.getMessage());
         }
+        else
+        {
+          if (remoteFile != null)
+          {
+            remoteFile.delete();
+            remoteFile = null;
+          }
+        }
 
         user = null;
         URI uri = URI.createFileURI(userSetup.getAbsolutePath());
@@ -499,6 +589,65 @@
       return this;
     }
 
+    public TestSynchronization exclude(String key)
+    {
+      SyncAction action = getPreferenceAction(key);
+      if (action != null)
+      {
+        String id = getID(action);
+        if (id != null)
+        {
+          resolve(id, SyncActionType.EXCLUDE);
+        }
+      }
+
+      return this;
+    }
+
+    public TestSynchronization pickLocal(String key)
+    {
+      SyncAction action = getPreferenceAction(key);
+      if (action != null)
+      {
+        SyncDelta delta = action.getLocalDelta();
+        SyncActionType location = SyncActionType.SET_LOCAL;
+
+        pick(action, delta, location);
+      }
+
+      return this;
+    }
+
+    public TestSynchronization pickRemote(String key)
+    {
+      SyncAction action = getPreferenceAction(key);
+      if (action != null)
+      {
+        SyncDelta delta = action.getRemoteDelta();
+        SyncActionType location = SyncActionType.SET_REMOTE;
+
+        pick(action, delta, location);
+      }
+
+      return this;
+    }
+
+    private void pick(SyncAction action, SyncDelta delta, SyncActionType location)
+    {
+      String id = getID(action);
+      if (id != null)
+      {
+        if (delta.getNewTask() == null)
+        {
+          resolve(id, SyncActionType.REMOVE);
+        }
+        else
+        {
+          resolve(id, location);
+        }
+      }
+    }
+
     public TestWorkstation commitAnd() throws IOException, NotCurrentException
     {
       commit();
diff --git a/plugins/org.eclipse.oomph.setup.sync/model/SetupSync.ecore b/plugins/org.eclipse.oomph.setup.sync/model/SetupSync.ecore
index ada7ba5..6c0b3a0 100644
--- a/plugins/org.eclipse.oomph.setup.sync/model/SetupSync.ecore
+++ b/plugins/org.eclipse.oomph.setup.sync/model/SetupSync.ecore
@@ -3,7 +3,11 @@
     xmlns:ecore="http://www.eclipse.org/emf/2002/Ecore" name="sync" nsURI="http://www.eclipse.org/oomph/setup/sync/1.0" nsPrefix="sync">
   <eClassifiers xsi:type="ecore:EClass" name="RemoteData" eSuperTypes="../../org.eclipse.oomph.setup/model/Setup.ecore#//SetupTaskContainer">
     <eStructuralFeatures xsi:type="ecore:EReference" name="policies" upperBound="-1"
-        eType="#//StringToSyncPolicyMapEntry" containment="true"/>
+        eType="#//StringToSyncPolicyMapEntry" containment="true">
+      <eAnnotations source="http:///org/eclipse/emf/ecore/util/ExtendedMetaData">
+        <details key="name" value="policy"/>
+      </eAnnotations>
+    </eStructuralFeatures>
   </eClassifiers>
   <eClassifiers xsi:type="ecore:EEnum" name="SyncPolicy">
     <eLiterals name="Include" literal="Include"/>
@@ -19,6 +23,10 @@
       <eAnnotations source="http://www.eclipse.org/emf/2002/GenModel">
         <details key="suppressedSetVisibility" value="true"/>
       </eAnnotations>
+      <eAnnotations source="http:///org/eclipse/emf/ecore/util/ExtendedMetaData">
+        <details key="kind" value="attribute"/>
+        <details key="name" value="id"/>
+      </eAnnotations>
     </eStructuralFeatures>
     <eStructuralFeatures xsi:type="ecore:EReference" name="oldTask" eType="ecore:EClass ../../org.eclipse.oomph.setup/model/Setup.ecore#//SetupTask">
       <eAnnotations source="http://www.eclipse.org/emf/2002/GenModel">
@@ -43,7 +51,12 @@
   </eClassifiers>
   <eClassifiers xsi:type="ecore:EClass" name="SyncAction">
     <eStructuralFeatures xsi:type="ecore:EAttribute" name="iD" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EString"
-        changeable="false" volatile="true" transient="true" derived="true" iD="true"/>
+        changeable="false" volatile="true" transient="true" derived="true" iD="true">
+      <eAnnotations source="http:///org/eclipse/emf/ecore/util/ExtendedMetaData">
+        <details key="kind" value="attribute"/>
+        <details key="name" value="id"/>
+      </eAnnotations>
+    </eStructuralFeatures>
     <eStructuralFeatures xsi:type="ecore:EReference" name="localDelta" eType="#//SyncDelta">
       <eAnnotations source="http://www.eclipse.org/emf/2002/GenModel">
         <details key="suppressedSetVisibility" value="true"/>
diff --git a/plugins/org.eclipse.oomph.setup.sync/src/org/eclipse/oomph/setup/internal/sync/RemoteDataProvider.java b/plugins/org.eclipse.oomph.setup.sync/src/org/eclipse/oomph/setup/internal/sync/RemoteDataProvider.java
index 6831f70..8bfc1f9 100644
--- a/plugins/org.eclipse.oomph.setup.sync/src/org/eclipse/oomph/setup/internal/sync/RemoteDataProvider.java
+++ b/plugins/org.eclipse.oomph.setup.sync/src/org/eclipse/oomph/setup/internal/sync/RemoteDataProvider.java
@@ -237,6 +237,8 @@
 
   private static void saveContent(HttpEntity entity, File file) throws IOException
   {
+    file.getParentFile().mkdirs();
+
     InputStream content = null;
     OutputStream out = null;
 
diff --git a/plugins/org.eclipse.oomph.setup.sync/src/org/eclipse/oomph/setup/internal/sync/Synchronization.java b/plugins/org.eclipse.oomph.setup.sync/src/org/eclipse/oomph/setup/internal/sync/Synchronization.java
index 2a75d56..aba4466 100644
--- a/plugins/org.eclipse.oomph.setup.sync/src/org/eclipse/oomph/setup/internal/sync/Synchronization.java
+++ b/plugins/org.eclipse.oomph.setup.sync/src/org/eclipse/oomph/setup/internal/sync/Synchronization.java
@@ -53,9 +53,9 @@
  */
 public class Synchronization
 {
-  private static final EClass USER_TYPE = SetupPackage.Literals.USER;
+  public static final EClass USER_TYPE = SetupPackage.Literals.USER;
 
-  private static final EClass REMOTE_DATA_TYPE = SyncPackage.Literals.REMOTE_DATA;
+  public static final EClass REMOTE_DATA_TYPE = SyncPackage.Literals.REMOTE_DATA;
 
   private final ResourceSet resourceSet = SyncUtil.createResourceSet();
 
@@ -69,7 +69,7 @@
 
   private final WorkingCopy remoteWorkingCopy;
 
-  private final EMap<String, SyncPolicy> remotePolicies;
+  private final EMap<String, SyncPolicy> policies;
 
   private final Map<String, SyncAction> actions;
 
@@ -88,8 +88,7 @@
     remoteWorkingCopy = createRemoteWorkingCopy();
     localWorkingCopy = createLocalWorkingCopy();
 
-    remotePolicies = null;
-    // remotePolicies = getPolicies(remoteWorkingCopy);
+    policies = getPolicies(remoteWorkingCopy);
 
     // Compute remote deltas first to make sure that new local tasks don't pick remotely existing IDs.
     Map<String, SyncDelta> remoteDeltas = computeRemoteDeltas(remoteWorkingCopy);
@@ -114,39 +113,24 @@
 
   public EMap<String, SyncPolicy> getRemotePolicies()
   {
-    return remotePolicies;
+    return policies;
   }
 
   private WorkingCopy createRemoteWorkingCopy() throws IOException
   {
-    return synchronizer.getRemoteSnapshot().createWorkingCopy();
+    Snapshot snapshot = synchronizer.getRemoteSnapshot();
+    return createWorkingCopy(snapshot, REMOTE_DATA_TYPE);
   }
 
   private WorkingCopy createLocalWorkingCopy() throws IOException
   {
-    return synchronizer.getLocalSnapshot().createWorkingCopy();
+    Snapshot snapshot = synchronizer.getLocalSnapshot();
+    return createWorkingCopy(snapshot, USER_TYPE);
   }
 
-  private EMap<String, SyncPolicy> getPolicies(WorkingCopy remoteWorkingCopy)
+  private WorkingCopy createWorkingCopy(Snapshot snapshot, EClass eClass) throws IOException
   {
-    File file = remoteWorkingCopy.getTmpFile();
-    RemoteData remoteData = loadObject(file, REMOTE_DATA_TYPE);
-    return remoteData.getPolicies();
-  }
-
-  private Map<String, SyncDelta> computeRemoteDeltas(WorkingCopy remoteWorkingCopy)
-  {
-    return computeDeltas(remoteWorkingCopy, REMOTE_DATA_TYPE);
-  }
-
-  private Map<String, SyncDelta> computeLocalDeltas(WorkingCopy localWorkingCopy)
-  {
-    return computeDeltas(localWorkingCopy, USER_TYPE);
-  }
-
-  private Map<String, SyncDelta> computeDeltas(WorkingCopy workingCopy, EClass eClass)
-  {
-    Snapshot snapshot = workingCopy.getSnapshot();
+    WorkingCopy workingCopy = snapshot.createWorkingCopy();
 
     File oldFile = snapshot.getOldFile();
     if (!oldFile.exists())
@@ -168,6 +152,38 @@
       }
     }
 
+    return workingCopy;
+  }
+
+  private EMap<String, SyncPolicy> getPolicies(WorkingCopy remoteWorkingCopy)
+  {
+    File file = remoteWorkingCopy.getTmpFile();
+    RemoteData remoteData = loadObject(file, REMOTE_DATA_TYPE);
+    return remoteData.getPolicies();
+  }
+
+  private boolean isIncluded(String id)
+  {
+    return SyncPolicy.EXCLUDE != policies.get(id);
+  }
+
+  private Map<String, SyncDelta> computeRemoteDeltas(WorkingCopy remoteWorkingCopy)
+  {
+    return computeDeltas(remoteWorkingCopy, REMOTE_DATA_TYPE);
+  }
+
+  private Map<String, SyncDelta> computeLocalDeltas(WorkingCopy localWorkingCopy)
+  {
+    return computeDeltas(localWorkingCopy, USER_TYPE);
+  }
+
+  private Map<String, SyncDelta> computeDeltas(WorkingCopy workingCopy, EClass eClass)
+  {
+    Snapshot snapshot = workingCopy.getSnapshot();
+
+    File oldFile = snapshot.getOldFile();
+    File tmpFile = workingCopy.getTmpFile();
+
     SetupTaskContainer oldData = loadObject(oldFile, eClass);
     SetupTaskContainer newData = loadObject(tmpFile, eClass);
 
@@ -294,24 +310,29 @@
     for (Map.Entry<String, SetupTask> oldEntry : oldTasks.entrySet())
     {
       String id = oldEntry.getKey();
-
-      SetupTask oldTask = oldEntry.getValue();
-      SetupTask newTask = newTasks.remove(id);
-
-      SyncDelta delta = compareTasks(id, oldTask, newTask);
-      if (delta != null)
+      if (isIncluded(id))
       {
-        deltas.put(id, delta);
+        SetupTask oldTask = oldEntry.getValue();
+        SetupTask newTask = newTasks.remove(id);
+
+        SyncDelta delta = compareTasks(id, oldTask, newTask);
+        if (delta != null)
+        {
+          deltas.put(id, delta);
+        }
       }
     }
 
     for (Map.Entry<String, SetupTask> newEntry : newTasks.entrySet())
     {
       String id = newEntry.getKey();
-      SetupTask newTask = newEntry.getValue();
+      if (isIncluded(id))
+      {
+        SetupTask newTask = newEntry.getValue();
 
-      SyncDelta delta = compareTasks(id, null, newTask);
-      deltas.put(id, delta);
+        SyncDelta delta = compareTasks(id, null, newTask);
+        deltas.put(id, delta);
+      }
     }
 
     return deltas;
@@ -502,9 +523,13 @@
 
       for (Map.Entry<String, SyncAction> entry : actions.entrySet())
       {
-        String id = entry.getKey();
         SyncAction action = entry.getValue();
 
+        if (action.getEffectiveType() == SyncActionType.CONFLICT)
+        {
+          String id = entry.getKey();
+          unresolvedActions.put(id, action);
+        }
       }
     }
 
@@ -585,18 +610,26 @@
           throw new ConflictException(action);
 
         case SET_LOCAL:
+          include(id);
           applySetAction(taskContainer, tasks, id, action.getLocalDelta());
           break;
 
         case SET_REMOTE:
+          include(id);
           applySetAction(taskContainer, tasks, id, action.getRemoteDelta());
           break;
 
         case REMOVE:
+          include(id);
           applyRemoveAction(taskContainer, tasks, action.getLocalDelta());
           applyRemoveAction(taskContainer, tasks, action.getRemoteDelta());
           break;
 
+        case EXCLUDE:
+          exclude(id);
+          applyRemoveAction(taskContainer, tasks, action.getRemoteDelta());
+          break;
+
         default:
           // Do nothing.
           break;
@@ -636,7 +669,9 @@
   {
     if (delta != null)
     {
-      SetupTask oldTask = delta.getOldTask();
+      String id = delta.getID();
+
+      SetupTask oldTask = tasks.get(id);
       if (oldTask != null)
       {
         EcoreUtil.remove(oldTask);
@@ -644,6 +679,16 @@
     }
   }
 
+  private void include(String id)
+  {
+    policies.put(id, SyncPolicy.INCLUDE);
+  }
+
+  private void exclude(String id)
+  {
+    policies.put(id, SyncPolicy.EXCLUDE);
+  }
+
   public void dispose()
   {
     if (!disposed)
@@ -707,6 +752,8 @@
     {
       if (msg.getFeature() == SyncPackage.Literals.SYNC_ACTION__RESOLVED_TYPE && !msg.isTouch())
       {
+        unresolvedActions = null;
+
         SyncAction action = (SyncAction)getTarget();
         synchronizer.actionResolved(action, id);
       }
diff --git a/plugins/org.eclipse.oomph.setup.sync/src/org/eclipse/oomph/setup/sync/RemoteData.java b/plugins/org.eclipse.oomph.setup.sync/src/org/eclipse/oomph/setup/sync/RemoteData.java
index 4b24700..893892e 100644
--- a/plugins/org.eclipse.oomph.setup.sync/src/org/eclipse/oomph/setup/sync/RemoteData.java
+++ b/plugins/org.eclipse.oomph.setup.sync/src/org/eclipse/oomph/setup/sync/RemoteData.java
@@ -45,6 +45,7 @@
    * @return the value of the '<em>Policies</em>' map.
    * @see org.eclipse.oomph.setup.sync.SyncPackage#getRemoteData_Policies()
    * @model mapType="org.eclipse.oomph.setup.sync.StringToSyncPolicyMapEntry<org.eclipse.emf.ecore.EString, org.eclipse.oomph.setup.sync.SyncPolicy>"
+   *        extendedMetaData="name='policy'"
    * @generated
    */
   EMap<String, SyncPolicy> getPolicies();
diff --git a/plugins/org.eclipse.oomph.setup.sync/src/org/eclipse/oomph/setup/sync/SyncAction.java b/plugins/org.eclipse.oomph.setup.sync/src/org/eclipse/oomph/setup/sync/SyncAction.java
index d50c60a..9597e50 100644
--- a/plugins/org.eclipse.oomph.setup.sync/src/org/eclipse/oomph/setup/sync/SyncAction.java
+++ b/plugins/org.eclipse.oomph.setup.sync/src/org/eclipse/oomph/setup/sync/SyncAction.java
@@ -46,6 +46,7 @@
    * @return the value of the '<em>ID</em>' attribute.
    * @see org.eclipse.oomph.setup.sync.SyncPackage#getSyncAction_ID()
    * @model id="true" transient="true" changeable="false" volatile="true" derived="true"
+   *        extendedMetaData="kind='attribute' name='id'"
    * @generated
    */
   String getID();
diff --git a/plugins/org.eclipse.oomph.setup.sync/src/org/eclipse/oomph/setup/sync/SyncDelta.java b/plugins/org.eclipse.oomph.setup.sync/src/org/eclipse/oomph/setup/sync/SyncDelta.java
index 3cfba55..deca65f 100644
--- a/plugins/org.eclipse.oomph.setup.sync/src/org/eclipse/oomph/setup/sync/SyncDelta.java
+++ b/plugins/org.eclipse.oomph.setup.sync/src/org/eclipse/oomph/setup/sync/SyncDelta.java
@@ -46,6 +46,7 @@
    * @return the value of the '<em>ID</em>' attribute.
    * @see org.eclipse.oomph.setup.sync.SyncPackage#getSyncDelta_ID()
    * @model id="true" required="true" suppressedSetVisibility="true"
+   *        extendedMetaData="kind='attribute' name='id'"
    * @generated
    */
   String getID();
diff --git a/plugins/org.eclipse.oomph.setup.sync/src/org/eclipse/oomph/setup/sync/impl/SyncPackageImpl.java b/plugins/org.eclipse.oomph.setup.sync/src/org/eclipse/oomph/setup/sync/impl/SyncPackageImpl.java
index 6a56bfc..f33494f 100644
--- a/plugins/org.eclipse.oomph.setup.sync/src/org/eclipse/oomph/setup/sync/impl/SyncPackageImpl.java
+++ b/plugins/org.eclipse.oomph.setup.sync/src/org/eclipse/oomph/setup/sync/impl/SyncPackageImpl.java
@@ -507,6 +507,24 @@
 
     // Create resource
     createResource(eNS_URI);
+
+    // Create annotations
+    // http:///org/eclipse/emf/ecore/util/ExtendedMetaData
+    createExtendedMetaDataAnnotations();
+  }
+
+  /**
+   * Initializes the annotations for <b>http:///org/eclipse/emf/ecore/util/ExtendedMetaData</b>.
+   * <!-- begin-user-doc -->
+   * <!-- end-user-doc -->
+   * @generated
+   */
+  protected void createExtendedMetaDataAnnotations()
+  {
+    String source = "http:///org/eclipse/emf/ecore/util/ExtendedMetaData";
+    addAnnotation(getRemoteData_Policies(), source, new String[] { "name", "policy" });
+    addAnnotation(getSyncDelta_ID(), source, new String[] { "kind", "attribute", "name", "id" });
+    addAnnotation(getSyncAction_ID(), source, new String[] { "kind", "attribute", "name", "id" });
   }
 
 } // SyncPackageImpl
diff --git a/plugins/org.eclipse.oomph.tests/src/org/eclipse/oomph/tests/AbstractTest.java b/plugins/org.eclipse.oomph.tests/src/org/eclipse/oomph/tests/AbstractTest.java
index 50188fc..447dee2 100644
--- a/plugins/org.eclipse.oomph.tests/src/org/eclipse/oomph/tests/AbstractTest.java
+++ b/plugins/org.eclipse.oomph.tests/src/org/eclipse/oomph/tests/AbstractTest.java
@@ -14,6 +14,7 @@
 import org.eclipse.oomph.util.IOUtil;
 import org.eclipse.oomph.util.OomphPlugin;
 import org.eclipse.oomph.util.OomphPlugin.BundleFile;
+import org.eclipse.oomph.util.StringUtil;
 
 import org.eclipse.core.runtime.IProgressMonitor;
 
@@ -22,16 +23,36 @@
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.rules.TestName;
+import org.junit.rules.TestWatcher;
+import org.junit.runner.Description;
 
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
 import java.io.File;
 import java.io.IOException;
 import java.io.PrintStream;
+import java.util.Iterator;
+import java.util.List;
 
 /**
  * @author Eike Stepper
  */
 public abstract class AbstractTest extends CoreMatchers
 {
+  private static final String[] FILTERS = { //
+      "org.eclipse.jdt.internal.junit.runner.", //
+      "org.eclipse.jdt.internal.junit.ui.", //
+      "org.eclipse.jdt.internal.junit4.runner.", //
+      "org.junit.", //
+      "sun.reflect.", //
+      "java.lang.reflect.Method.invoke(", //
+      "junit.framework.Assert", //
+      "junit.framework.TestCase", //
+      "junit.framework.TestResult", //
+      "junit.framework.TestResult$1", //
+      "junit.framework.TestSuite", //
+  };
+
   private static final PrintStream LOG = System.out;
 
   public static final IProgressMonitor LOGGER = new IProgressMonitor()
@@ -91,7 +112,10 @@
   };
 
   @Rule
-  public TestName testName = new TestName();
+  public final TestWatcher failurePrinter = new FailurePrinter();
+
+  @Rule
+  public final TestName testName = new TestName();
 
   private File userHome;
 
@@ -110,21 +134,6 @@
     LOGGER.setTaskName(null);
   }
 
-  public static File createTempFolder()
-  {
-    try
-    {
-      File folder = File.createTempFile("test-", "");
-      folder.delete();
-      folder.mkdirs();
-      return folder;
-    }
-    catch (IOException ex)
-    {
-      throw new IORuntimeException(ex);
-    }
-  }
-
   public File getUserHome()
   {
     if (userHome == null)
@@ -166,6 +175,21 @@
     }
   }
 
+  public static File createTempFolder()
+  {
+    try
+    {
+      File folder = File.createTempFile("test-", "");
+      folder.delete();
+      folder.mkdirs();
+      return folder;
+    }
+    catch (IOException ex)
+    {
+      throw new IORuntimeException(ex);
+    }
+  }
+
   public static void log()
   {
     LOG.println();
@@ -173,11 +197,54 @@
 
   public static void log(Object object)
   {
-    LOG.println(object);
+    if (object instanceof Throwable)
+    {
+      Throwable ex = (Throwable)object;
+      printStackTrace(ex);
+    }
+    else
+    {
+      LOG.println(object);
+    }
+  }
+
+  public static void printStackTrace(Throwable ex)
+  {
+    ByteArrayOutputStream baos = new ByteArrayOutputStream();
+    ex.printStackTrace(new PrintStream(baos));
+
+    List<String> lines = IOUtil.readLines(new ByteArrayInputStream(baos.toByteArray()), "UTF-8");
+    for (Iterator<String> it = lines.iterator(); it.hasNext();)
+    {
+      String line = it.next().trim();
+      for (int i = 0; i < FILTERS.length; i++)
+      {
+        String filter = FILTERS[i];
+        if (line.startsWith("at " + filter))
+        {
+          it.remove();
+          continue;
+        }
+      }
+    }
+
+    System.err.println(StringUtil.implode(lines, '\n'));
   }
 
   public static org.hamcrest.Matcher<java.lang.Object> isNull()
   {
     return org.hamcrest.core.IsNull.nullValue();
   }
+
+  /**
+   * @author Eike Stepper
+   */
+  private static final class FailurePrinter extends TestWatcher
+  {
+    @Override
+    protected void failed(Throwable ex, Description description)
+    {
+      printStackTrace(ex);
+    }
+  }
 }
diff --git a/plugins/org.eclipse.oomph.util/src/org/eclipse/oomph/util/IOUtil.java b/plugins/org.eclipse.oomph.util/src/org/eclipse/oomph/util/IOUtil.java
index 8a7b246..1950291 100644
--- a/plugins/org.eclipse.oomph.util/src/org/eclipse/oomph/util/IOUtil.java
+++ b/plugins/org.eclipse.oomph.util/src/org/eclipse/oomph/util/IOUtil.java
@@ -682,20 +682,11 @@
     if (file.exists())
     {
       InputStream in = null;
-      Reader reader = null;
-      BufferedReader bufferedReader = null;
 
       try
       {
         in = new FileInputStream(file);
-        reader = charsetName == null ? new InputStreamReader(in) : new InputStreamReader(in, charsetName);
-        bufferedReader = new BufferedReader(reader);
-
-        String line;
-        while ((line = bufferedReader.readLine()) != null)
-        {
-          lines.add(line);
-        }
+        return readLines(in, charsetName);
       }
       catch (IOException ex)
       {
@@ -703,8 +694,6 @@
       }
       finally
       {
-        closeSilent(bufferedReader);
-        closeSilent(reader);
         closeSilent(in);
       }
     }
@@ -712,10 +701,42 @@
     return lines;
   }
 
+  public static List<String> readLines(InputStream in, String charsetName)
+  {
+    List<String> lines = new ArrayList<String>();
+
+    Reader reader = null;
+    BufferedReader bufferedReader = null;
+
+    try
+    {
+      reader = charsetName == null ? new InputStreamReader(in) : new InputStreamReader(in, charsetName);
+      bufferedReader = new BufferedReader(reader);
+
+      String line;
+      while ((line = bufferedReader.readLine()) != null)
+      {
+        lines.add(line);
+      }
+    }
+    catch (IOException ex)
+    {
+      throw new IORuntimeException(ex);
+    }
+    finally
+    {
+      closeSilent(bufferedReader);
+      closeSilent(reader);
+    }
+
+    return lines;
+  }
+
   public static void writeLines(File file, String charsetName, List<String> lines)
   {
     mkdirs(file.getParentFile());
     OutputStream out = null;
+
     try
     {
       out = new FileOutputStream(file);