[470900] Eclipse Mars repository for EPP causing problems for existing installations that have used Oomph

https://bugs.eclipse.org/bugs/show_bug.cgi?id=470900
diff --git a/plugins/org.eclipse.oomph.jreinfo.ui/META-INF/MANIFEST.MF b/plugins/org.eclipse.oomph.jreinfo.ui/META-INF/MANIFEST.MF
index 9a35e7d..65499f3 100644
--- a/plugins/org.eclipse.oomph.jreinfo.ui/META-INF/MANIFEST.MF
+++ b/plugins/org.eclipse.oomph.jreinfo.ui/META-INF/MANIFEST.MF
@@ -1,7 +1,7 @@
 Manifest-Version: 1.0
 Bundle-ManifestVersion: 2
 Bundle-SymbolicName: org.eclipse.oomph.jreinfo.ui;singleton:=true
-Bundle-Version: 1.1.0.qualifier
+Bundle-Version: 1.2.0.qualifier
 Bundle-Localization: plugin
 Bundle-Name: %pluginName
 Bundle-Vendor: %providerName
@@ -11,4 +11,4 @@
 Require-Bundle: org.eclipse.core.runtime;bundle-version="[3.5.0,4.0.0)",
  org.eclipse.oomph.ui;bundle-version="[1.1.0,2.0.0)",
  org.eclipse.oomph.jreinfo;bundle-version="[1.1.0,2.0.0)";visibility:=reexport
-Export-Package: org.eclipse.oomph.jreinfo.ui;version="1.1.0";x-internal:=true
+Export-Package: org.eclipse.oomph.jreinfo.ui;version="1.2.0";x-internal:=true
diff --git a/plugins/org.eclipse.oomph.jreinfo.ui/pom.xml b/plugins/org.eclipse.oomph.jreinfo.ui/pom.xml
index e3bae70..9f40aec 100644
--- a/plugins/org.eclipse.oomph.jreinfo.ui/pom.xml
+++ b/plugins/org.eclipse.oomph.jreinfo.ui/pom.xml
@@ -20,7 +20,7 @@
   </parent>
   <groupId>org.eclipse.oomph</groupId>
   <artifactId>org.eclipse.oomph.jreinfo.ui</artifactId>
-  <version>1.1.0-SNAPSHOT</version>
+  <version>1.2.0-SNAPSHOT</version>
   <packaging>eclipse-plugin</packaging>
 
   <build>
diff --git a/plugins/org.eclipse.oomph.jreinfo.ui/src/org/eclipse/oomph/jreinfo/ui/JREComposite.java b/plugins/org.eclipse.oomph.jreinfo.ui/src/org/eclipse/oomph/jreinfo/ui/JREComposite.java
index 4e717ba..8627957 100644
--- a/plugins/org.eclipse.oomph.jreinfo.ui/src/org/eclipse/oomph/jreinfo/ui/JREComposite.java
+++ b/plugins/org.eclipse.oomph.jreinfo.ui/src/org/eclipse/oomph/jreinfo/ui/JREComposite.java
@@ -342,7 +342,7 @@
       rootFolder = rootFolder.getParentFile();
     }
 
-    String filterPath = rootFolder == null ? PropertiesUtil.USER_HOME : rootFolder.getAbsolutePath();
+    String filterPath = rootFolder == null ? PropertiesUtil.getUserHome() : rootFolder.getAbsolutePath();
 
     DirectoryDialog dialog = new DirectoryDialog(getShell());
     dialog.setText(JREDialog.TITLE);
diff --git a/plugins/org.eclipse.oomph.jreinfo/META-INF/MANIFEST.MF b/plugins/org.eclipse.oomph.jreinfo/META-INF/MANIFEST.MF
index 3df063f..1423f50 100644
--- a/plugins/org.eclipse.oomph.jreinfo/META-INF/MANIFEST.MF
+++ b/plugins/org.eclipse.oomph.jreinfo/META-INF/MANIFEST.MF
@@ -1,7 +1,7 @@
 Manifest-Version: 1.0
 Bundle-ManifestVersion: 2
 Bundle-SymbolicName: org.eclipse.oomph.jreinfo
-Bundle-Version: 1.1.0.qualifier
+Bundle-Version: 1.2.0.qualifier
 Bundle-Localization: plugin
 Bundle-Name: %pluginName
 Bundle-Vendor: %providerName
@@ -12,5 +12,5 @@
 Require-Bundle: org.eclipse.core.runtime;bundle-version="[3.5.0,4.0.0)",
  org.eclipse.oomph.util;bundle-version="[1.1.0,2.0.0)",
  org.eclipse.oomph.extractor.lib;bundle-version="[1.1.0,2.0.0)"
-Export-Package: org.eclipse.oomph.internal.jreinfo;version="1.1.0";x-internal:=true,
- org.eclipse.oomph.jreinfo;version="1.1.0";x-internal:=true
+Export-Package: org.eclipse.oomph.internal.jreinfo;version="1.2.0";x-internal:=true,
+ org.eclipse.oomph.jreinfo;version="1.2.0";x-internal:=true
diff --git a/plugins/org.eclipse.oomph.jreinfo/pom.xml b/plugins/org.eclipse.oomph.jreinfo/pom.xml
index c128af7..bad327b 100644
--- a/plugins/org.eclipse.oomph.jreinfo/pom.xml
+++ b/plugins/org.eclipse.oomph.jreinfo/pom.xml
@@ -20,7 +20,7 @@
   </parent>
   <groupId>org.eclipse.oomph</groupId>
   <artifactId>org.eclipse.oomph.jreinfo</artifactId>
-  <version>1.1.0-SNAPSHOT</version>
+  <version>1.2.0-SNAPSHOT</version>
   <packaging>eclipse-plugin</packaging>
 
   <build>
diff --git a/plugins/org.eclipse.oomph.jreinfo/src/org/eclipse/oomph/jreinfo/JREInfo.java b/plugins/org.eclipse.oomph.jreinfo/src/org/eclipse/oomph/jreinfo/JREInfo.java
index bb92a2a..3314915 100644
--- a/plugins/org.eclipse.oomph.jreinfo/src/org/eclipse/oomph/jreinfo/JREInfo.java
+++ b/plugins/org.eclipse.oomph.jreinfo/src/org/eclipse/oomph/jreinfo/JREInfo.java
@@ -85,9 +85,10 @@
 
       if (!SKIP_USER_HOME)
       {
-        jreInfo = searchFolder(jreInfo, PropertiesUtil.USER_HOME);
-        jreInfo = searchFolder(jreInfo, PropertiesUtil.USER_HOME + "/java");
-        jreInfo = searchFolder(jreInfo, PropertiesUtil.USER_HOME + "/jvm");
+        String userHome = PropertiesUtil.getUserHome();
+        jreInfo = searchFolder(jreInfo, userHome);
+        jreInfo = searchFolder(jreInfo, userHome + "/java");
+        jreInfo = searchFolder(jreInfo, userHome + "/jvm");
       }
 
       for (int i = 0; i < EXTRA_SEARCH_PATH.length; i++)
diff --git a/plugins/org.eclipse.oomph.p2.core/src/org/eclipse/oomph/p2/internal/core/AgentManagerImpl.java b/plugins/org.eclipse.oomph.p2.core/src/org/eclipse/oomph/p2/internal/core/AgentManagerImpl.java
index ce641f8..36e63b6 100644
--- a/plugins/org.eclipse.oomph.p2.core/src/org/eclipse/oomph/p2/internal/core/AgentManagerImpl.java
+++ b/plugins/org.eclipse.oomph.p2.core/src/org/eclipse/oomph/p2/internal/core/AgentManagerImpl.java
@@ -60,7 +60,7 @@
 
   public AgentManagerImpl()
   {
-    this(new File(PropertiesUtil.USER_HOME));
+    this(new File(PropertiesUtil.getUserHome()));
   }
 
   public AgentManagerImpl(final File userHome)
diff --git a/plugins/org.eclipse.oomph.p2.core/src/org/eclipse/oomph/p2/internal/core/CachingRepositoryManager.java b/plugins/org.eclipse.oomph.p2.core/src/org/eclipse/oomph/p2/internal/core/CachingRepositoryManager.java
index c5bc322..6e48c8a 100644
--- a/plugins/org.eclipse.oomph.p2.core/src/org/eclipse/oomph/p2/internal/core/CachingRepositoryManager.java
+++ b/plugins/org.eclipse.oomph.p2.core/src/org/eclipse/oomph/p2/internal/core/CachingRepositoryManager.java
@@ -10,7 +10,6 @@
  */
 package org.eclipse.oomph.p2.internal.core;
 
-import org.eclipse.oomph.util.ObjectUtil;
 import org.eclipse.oomph.util.PropertiesUtil;
 import org.eclipse.oomph.util.ReflectUtil;
 import org.eclipse.oomph.util.ReflectUtil.ReflectionException;
@@ -20,6 +19,7 @@
 import org.eclipse.core.runtime.SubMonitor;
 import org.eclipse.equinox.internal.p2.artifact.repository.ArtifactRepositoryManager;
 import org.eclipse.equinox.internal.p2.metadata.repository.MetadataRepositoryManager;
+import org.eclipse.equinox.internal.p2.repository.Transport;
 import org.eclipse.equinox.internal.p2.repository.helpers.AbstractRepositoryManager;
 import org.eclipse.equinox.internal.p2.repository.helpers.LocationProperties;
 import org.eclipse.equinox.internal.provisional.p2.repository.RepositoryEvent;
@@ -34,7 +34,9 @@
 import java.lang.reflect.Method;
 import java.net.URI;
 import java.net.URISyntaxException;
-import java.util.LinkedHashMap;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
 import java.util.Map;
 
 /**
@@ -49,8 +51,6 @@
 
   private static final Method METHOD_basicGetRepository = ReflectUtil.getMethod(AbstractRepositoryManager.class, "basicGetRepository", URI.class);
 
-  // private static final Method METHOD_checkNotFound = ReflectUtil.getMethod(AbstractRepositoryManager.class, "checkNotFound", URI.class);
-
   private static final Method METHOD_fail = ReflectUtil.getMethod(AbstractRepositoryManager.class, "fail", URI.class, int.class);
 
   private static final Method METHOD_addRepository1 = ReflectUtil.getMethod(AbstractRepositoryManager.class, "addRepository", URI.class, boolean.class,
@@ -63,9 +63,6 @@
 
   private static final Method METHOD_getAllSuffixes = ReflectUtil.getMethod(AbstractRepositoryManager.class, "getAllSuffixes");
 
-  private static final Method METHOD_sortSuffixes = ReflectUtil.getMethod(AbstractRepositoryManager.class, "sortSuffixes", String[].class, URI.class,
-      String[].class);
-
   private static final Method METHOD_loadRepository = ReflectUtil.getMethod(AbstractRepositoryManager.class, "loadRepository", URI.class, String.class,
       String.class, int.class, SubMonitor.class);
 
@@ -74,7 +71,6 @@
 
   private static final Method METHOD_removeRepository = ReflectUtil.getMethod(AbstractRepositoryManager.class, "removeRepository", URI.class, boolean.class);
 
-  // private static final Method METHOD_rememberNotFound = ReflectUtil.getMethod(AbstractRepositoryManager.class, "rememberNotFound", URI.class);
 
   private static final Method METHOD_exitLoad = ReflectUtil.getMethod(AbstractRepositoryManager.class, "exitLoad", URI.class);
 
@@ -83,8 +79,6 @@
 
   private static final String PROPERTY_VERSION = "version";
 
-  private static final String PROPERTY_GENERATED = "generated";
-
   private final AbstractRepositoryManager<T> delegate;
 
   private final int repositoryType;
@@ -95,6 +89,16 @@
   {
     this.delegate = delegate;
     this.repositoryType = repositoryType;
+
+    if (transport == null)
+    {
+      Object t = delegate.getAgent().getService(Transport.SERVICE_NAME);
+      if (t instanceof CachingTransport)
+      {
+        transport = (CachingTransport)t;
+      }
+    }
+
     this.transport = transport;
   }
 
@@ -116,10 +120,6 @@
         return result;
       }
 
-      // if (checkNotFound(location))
-      // {
-      // fail(location, ProvisionException.REPOSITORY_NOT_FOUND);
-      // }
 
       // Add the repository first so that it will be enabled, but don't send add event until after the load.
       added = addRepository(location, true, false);
@@ -127,7 +127,7 @@
       LocationProperties indexFile = loadIndexFile(location, sub.newChild(15));
       String[] preferredOrder = getPreferredRepositorySearchOrder(indexFile);
       String[] allSuffixes = getAllSuffixes();
-      String[] suffixes = sortSuffixes(allSuffixes, location, preferredOrder);
+      String[] suffixes = sortSuffixes(allSuffixes, preferredOrder);
 
       sub = SubMonitor.convert(sub, NLS.bind("Adding repository {0}", location), suffixes.length * 100);
       ProvisionException failure = null;
@@ -154,12 +154,7 @@
           if (result != null)
           {
             addRepository(result, false, suffixes[i]);
-
-            if (!indexFile.exists() || preferredOrder.length == 0)
-            {
-              cacheIndexFile(location, suffixes[i], allSuffixes);
-            }
-
+            cacheIndexFile(location, suffixes[i]);
             break;
           }
         }
@@ -182,11 +177,6 @@
         {
           delegate.removeRepository(location);
         }
-        // else if (failure == null || failure.getStatus().getCode() != ProvisionException.REPOSITORY_FAILED_AUTHENTICATION
-        // && failure.getStatus().getCode() != ProvisionException.REPOSITORY_FAILED_READ)
-        // {
-        // rememberNotFound(location);
-        // }
 
         if (failure != null)
         {
@@ -230,40 +220,30 @@
     }
   }
 
-  private void cacheIndexFile(URI location, String suffix, String[] allSuffixes)
+  private void cacheIndexFile(URI location, String suffix)
   {
     if ("file".equals(location.getScheme()))
     {
       return;
     }
 
-    Map<String, String> properties;
-
     File cachedIndexFile = getCachedIndexFile(location);
-    if (cachedIndexFile.exists())
+
+    Map<String, String> properties = PropertiesUtil.getProperties(cachedIndexFile);
+    if (!properties.containsKey(PROPERTY_VERSION))
     {
-      properties = PropertiesUtil.loadProperties(cachedIndexFile);
+      properties.put(PROPERTY_VERSION, "1");
+    }
+
+    if (repositoryType == IRepository.TYPE_METADATA)
+    {
+      properties.put("metadata.repository.factory.order", suffix);
     }
     else
     {
-      properties = new LinkedHashMap<String, String>();
-      properties.put(PROPERTY_VERSION, "1");
-      properties.put(PROPERTY_GENERATED, "true");
+      properties.put("artifact.repository.factory.order", suffix);
     }
 
-    String prefix = repositoryType == IRepository.TYPE_METADATA ? "metadata" : "artifact";
-
-    StringBuilder builder = new StringBuilder(suffix);
-    for (String otherSuffix : allSuffixes)
-    {
-      if (!ObjectUtil.equals(otherSuffix, suffix))
-      {
-        builder.append(",");
-        builder.append(otherSuffix);
-      }
-    }
-
-    properties.put(prefix + ".repository.factory.order", builder.toString() + ",!");
     PropertiesUtil.saveProperties(cachedIndexFile, properties, false);
   }
 
@@ -331,9 +311,21 @@
     return (String[])ReflectUtil.invokeMethod(METHOD_getAllSuffixes, delegate);
   }
 
-  protected String[] sortSuffixes(String[] suffixes, URI location, String[] preferredOrder)
+  private String[] sortSuffixes(String[] allSuffixes, String[] preferredOrder)
   {
-    return (String[])ReflectUtil.invokeMethod(METHOD_sortSuffixes, delegate, suffixes, location, preferredOrder);
+    List<String> suffixes = new ArrayList<String>(Arrays.asList(allSuffixes));
+
+    for (int i = preferredOrder.length - 1; i >= 0; --i)
+    {
+      String suffix = preferredOrder[i].trim();
+      if (!LocationProperties.END.equals(suffix))
+      {
+        suffixes.remove(suffix);
+        suffixes.add(0, suffix);
+      }
+    }
+
+    return suffixes.toArray(new String[suffixes.size()]);
   }
 
   @SuppressWarnings("unchecked")
@@ -378,7 +370,7 @@
   /**
    * @author Eike Stepper
    */
-  public static final class Metadata extends MetadataRepositoryManager
+  public static class Metadata extends MetadataRepositoryManager
   {
     private final CachingRepositoryManager<IInstallableUnit> loader;
 
@@ -398,7 +390,7 @@
   /**
    * @author Eike Stepper
    */
-  public static final class Artifact extends ArtifactRepositoryManager
+  public static class Artifact extends ArtifactRepositoryManager
   {
     private final CachingRepositoryManager<IArtifactKey> loader;
 
diff --git a/plugins/org.eclipse.oomph.p2.core/src/org/eclipse/oomph/p2/internal/core/CachingTransport.java b/plugins/org.eclipse.oomph.p2.core/src/org/eclipse/oomph/p2/internal/core/CachingTransport.java
index 2fa84d2..1306f21 100644
--- a/plugins/org.eclipse.oomph.p2.core/src/org/eclipse/oomph/p2/internal/core/CachingTransport.java
+++ b/plugins/org.eclipse.oomph.p2.core/src/org/eclipse/oomph/p2/internal/core/CachingTransport.java
@@ -28,7 +28,6 @@
 import org.eclipse.equinox.p2.core.IProvisioningAgent;
 
 import java.io.File;
-import java.io.FileFilter;
 import java.io.FileInputStream;
 import java.io.FileNotFoundException;
 import java.io.FileOutputStream;
@@ -48,6 +47,8 @@
 @SuppressWarnings("restriction")
 public class CachingTransport extends Transport
 {
+  public static final String SERVICE_NAME = Transport.SERVICE_NAME;
+
   private static final ThreadLocal<Stack<String>> REPOSITORY_LOCATIONS = new InheritableThreadLocal<Stack<String>>()
   {
     @Override
@@ -61,69 +62,35 @@
 
   private static boolean DEBUG = false;
 
-  private final Transport delegate;
-
   private final IProvisioningAgent agent;
 
   private final File cacheFolder;
 
+  private Transport delegate;
+
   public CachingTransport(Transport delegate, IProvisioningAgent agent)
   {
+    setDelegate(delegate);
+    this.agent = agent;
+
+    File folder = P2CorePlugin.getUserStateFolder(new File(PropertiesUtil.getUserHome()));
+    cacheFolder = new File(folder, "cache");
+    cacheFolder.mkdirs();
+  }
+
+  public final Transport getDelegate()
+  {
+    return delegate;
+  }
+
+  public final void setDelegate(Transport delegate)
+  {
     if (delegate instanceof CachingTransport)
     {
-      throw new IllegalArgumentException("CachingTransport should not be chained.");
+      throw new IllegalArgumentException("CachingTransport should not be chained");
     }
 
     this.delegate = delegate;
-    this.agent = agent;
-
-    File folder = P2CorePlugin.getUserStateFolder(new File(PropertiesUtil.USER_HOME));
-    cacheFolder = new File(folder, "cache");
-    if (cacheFolder.exists())
-    {
-      cleanupIndexFiles();
-    }
-    else
-    {
-      cacheFolder.mkdirs();
-    }
-  }
-
-  private void cleanupIndexFiles()
-  {
-    File markerFile = new File(cacheFolder, ".indexes_cleaned_up");
-    if (!markerFile.exists())
-    {
-      try
-      {
-        markerFile.createNewFile();
-      }
-      catch (IOException ex)
-      {
-        P2CorePlugin.INSTANCE.log(ex);
-        return;
-      }
-
-      File[] indexFiles = cacheFolder.listFiles(new FileFilter()
-      {
-        public boolean accept(File file)
-        {
-          return file.isFile() && file.getName().endsWith("_p2.index");
-        }
-      });
-
-      for (File indexFile : indexFiles)
-      {
-        try
-        {
-          IOUtil.deleteBestEffort(indexFile, false);
-        }
-        catch (Exception ex)
-        {
-          P2CorePlugin.INSTANCE.log(ex, IStatus.WARNING);
-        }
-      }
-    }
   }
 
   public File getCacheFile(URI uri)
@@ -144,12 +111,14 @@
       return delegate.download(uri, target, startPos, monitor);
     }
 
+
     synchronized (getLock(uri))
     {
       File cacheFile = getCacheFile(uri);
       if (cacheFile.length() > 0)
       {
         FileInputStream cacheInputStream = null;
+
         try
         {
           cacheInputStream = new FileInputStream(cacheFile);
diff --git a/plugins/org.eclipse.oomph.p2.tests/AgentTests.launch b/plugins/org.eclipse.oomph.p2.tests/AgentTests.launch
index 27ab114..bc6bc1c 100644
--- a/plugins/org.eclipse.oomph.p2.tests/AgentTests.launch
+++ b/plugins/org.eclipse.oomph.p2.tests/AgentTests.launch
@@ -33,8 +33,8 @@
 <stringAttribute key="pde.version" value="3.3"/>
 <stringAttribute key="product" value="org.eclipse.equinox.p2.director.app.product"/>
 <booleanAttribute key="run_in_ui_thread" value="true"/>
-<stringAttribute key="selected_target_plugins" value="com.ibm.icu@default:default,javax.annotation@default:default,javax.inject@default:default,javax.xml@default:default,org.apache.batik.css@default:default,org.apache.batik.util.gui@default:default,org.apache.batik.util@default:default,org.apache.commons.codec@default:default,org.apache.commons.logging@default:default,org.apache.httpcomponents.httpclient@default:default,org.apache.httpcomponents.httpcore@default:default,org.eclipse.ant.core@default:default,org.eclipse.compare.core@default:default,org.eclipse.core.commands@default:default,org.eclipse.core.contenttype@default:default,org.eclipse.core.databinding.observable@default:default,org.eclipse.core.databinding.property@default:default,org.eclipse.core.databinding@default:default,org.eclipse.core.expressions@default:default,org.eclipse.core.filesystem.java7@default:false,org.eclipse.core.filesystem.win32.x86_64@default:false,org.eclipse.core.filesystem@default:default,org.eclipse.core.jobs@default:default,org.eclipse.core.net.win32.x86_64@default:false,org.eclipse.core.net@default:default,org.eclipse.core.resources.win32.x86_64@default:false,org.eclipse.core.resources@default:default,org.eclipse.core.runtime.compatibility.registry@default:false,org.eclipse.core.runtime@default:true,org.eclipse.core.variables@default:default,org.eclipse.e4.core.commands@default:default,org.eclipse.e4.core.contexts@default:default,org.eclipse.e4.core.di.extensions@default:default,org.eclipse.e4.core.di@default:default,org.eclipse.e4.core.services@default:default,org.eclipse.e4.ui.bindings@default:default,org.eclipse.e4.ui.css.core@default:default,org.eclipse.e4.ui.css.swt.theme@default:default,org.eclipse.e4.ui.css.swt@default:default,org.eclipse.e4.ui.di@default:default,org.eclipse.e4.ui.model.workbench@default:default,org.eclipse.e4.ui.services@default:default,org.eclipse.e4.ui.widgets@default:default,org.eclipse.e4.ui.workbench.addons.swt@default:default,org.eclipse.e4.ui.workbench.renderers.swt@default:default,org.eclipse.e4.ui.workbench.swt@default:default,org.eclipse.e4.ui.workbench3@default:default,org.eclipse.e4.ui.workbench@default:default,org.eclipse.ecf.filetransfer@default:default,org.eclipse.ecf.identity@default:default,org.eclipse.ecf.provider.filetransfer.httpclient4.ssl@default:false,org.eclipse.ecf.provider.filetransfer.httpclient4@default:default,org.eclipse.ecf.provider.filetransfer.ssl@default:false,org.eclipse.ecf.provider.filetransfer@default:default,org.eclipse.ecf.ssl@default:false,org.eclipse.ecf@default:default,org.eclipse.emf.common@default:default,org.eclipse.emf.ecore.change@default:default,org.eclipse.emf.ecore.xmi@default:default,org.eclipse.emf.ecore@default:default,org.eclipse.equinox.app@default:default,org.eclipse.equinox.common@2:true,org.eclipse.equinox.ds@1:true,org.eclipse.equinox.frameworkadmin.equinox@default:default,org.eclipse.equinox.frameworkadmin@default:default,org.eclipse.equinox.p2.artifact.repository@default:default,org.eclipse.equinox.p2.core@default:default,org.eclipse.equinox.p2.director@default:default,org.eclipse.equinox.p2.engine@default:default,org.eclipse.equinox.p2.garbagecollector@default:default,org.eclipse.equinox.p2.jarprocessor@default:default,org.eclipse.equinox.p2.metadata.repository@default:default,org.eclipse.equinox.p2.metadata@default:default,org.eclipse.equinox.p2.operations@default:default,org.eclipse.equinox.p2.publisher.eclipse@default:default,org.eclipse.equinox.p2.publisher@default:default,org.eclipse.equinox.p2.repository.tools@default:default,org.eclipse.equinox.p2.repository@default:default,org.eclipse.equinox.p2.touchpoint.eclipse@default:default,org.eclipse.equinox.p2.touchpoint.natives@default:default,org.eclipse.equinox.p2.transport.ecf@default:default,org.eclipse.equinox.preferences@default:default,org.eclipse.equinox.registry@default:default,org.eclipse.equinox.security.win32.x86_64@default:false,org.eclipse.equinox.security@default:default,org.eclipse.equinox.simpleconfigurator.manipulator@default:default,org.eclipse.equinox.simpleconfigurator@1:true,org.eclipse.equinox.util@default:default,org.eclipse.help@default:default,org.eclipse.jface.databinding@default:default,org.eclipse.jface@default:default,org.eclipse.osgi.compatibility.state@default:false,org.eclipse.osgi.services@default:default,org.eclipse.osgi@-1:true,org.eclipse.swt.win32.win32.x86_64@default:false,org.eclipse.swt@default:default,org.eclipse.team.core@default:default,org.eclipse.ui.trace@default:default,org.eclipse.ui.workbench@default:default,org.eclipse.ui@default:default,org.hamcrest.core@default:default,org.junit@default:default,org.sat4j.core@default:default,org.sat4j.pb@default:default,org.w3c.css.sac@default:default,org.w3c.dom.events@default:default,org.w3c.dom.smil@default:default,org.w3c.dom.svg@default:default"/>
-<stringAttribute key="selected_workspace_plugins" value="org.eclipse.oomph.base@default:default,org.eclipse.oomph.p2.core@default:default,org.eclipse.oomph.p2.tests@default:default,org.eclipse.oomph.p2@default:default,org.eclipse.oomph.util@default:default"/>
+<stringAttribute key="selected_target_plugins" value="com.ibm.icu@default:default,javax.annotation@default:default,javax.inject@default:default,javax.xml@default:default,org.apache.batik.css@default:default,org.apache.batik.util.gui@default:default,org.apache.batik.util@default:default,org.apache.commons.codec@default:default,org.apache.commons.jxpath@default:default,org.apache.commons.logging@default:default,org.apache.httpcomponents.httpclient@default:default,org.apache.httpcomponents.httpcore@default:default,org.eclipse.ant.core@default:default,org.eclipse.compare.core@default:default,org.eclipse.core.commands@default:default,org.eclipse.core.contenttype@default:default,org.eclipse.core.databinding.observable@default:default,org.eclipse.core.databinding.property@default:default,org.eclipse.core.databinding@default:default,org.eclipse.core.expressions@default:default,org.eclipse.core.filesystem.java7@default:false,org.eclipse.core.filesystem.win32.x86_64@default:false,org.eclipse.core.filesystem@default:default,org.eclipse.core.jobs@default:default,org.eclipse.core.net.win32.x86_64@default:false,org.eclipse.core.net@default:default,org.eclipse.core.resources.win32.x86_64@default:false,org.eclipse.core.resources@default:default,org.eclipse.core.runtime.compatibility.registry@default:false,org.eclipse.core.runtime@default:true,org.eclipse.core.variables@default:default,org.eclipse.e4.core.commands@default:default,org.eclipse.e4.core.contexts@default:default,org.eclipse.e4.core.di.annotations@default:default,org.eclipse.e4.core.di.extensions@default:default,org.eclipse.e4.core.di@default:default,org.eclipse.e4.core.services@default:default,org.eclipse.e4.emf.xpath@default:default,org.eclipse.e4.ui.bindings@default:default,org.eclipse.e4.ui.css.core@default:default,org.eclipse.e4.ui.css.swt.theme@default:default,org.eclipse.e4.ui.css.swt@default:default,org.eclipse.e4.ui.di@default:default,org.eclipse.e4.ui.model.workbench@default:default,org.eclipse.e4.ui.services@default:default,org.eclipse.e4.ui.widgets@default:default,org.eclipse.e4.ui.workbench.addons.swt@default:default,org.eclipse.e4.ui.workbench.renderers.swt@default:default,org.eclipse.e4.ui.workbench.swt@default:default,org.eclipse.e4.ui.workbench3@default:default,org.eclipse.e4.ui.workbench@default:default,org.eclipse.ecf.filetransfer@default:default,org.eclipse.ecf.identity@default:default,org.eclipse.ecf.provider.filetransfer.httpclient4.ssl@default:false,org.eclipse.ecf.provider.filetransfer.httpclient4@default:default,org.eclipse.ecf.provider.filetransfer.ssl@default:false,org.eclipse.ecf.provider.filetransfer@default:default,org.eclipse.ecf.ssl@default:false,org.eclipse.ecf@default:default,org.eclipse.emf.common@default:default,org.eclipse.emf.ecore.change@default:default,org.eclipse.emf.ecore.xmi@default:default,org.eclipse.emf.ecore@default:default,org.eclipse.equinox.app@default:default,org.eclipse.equinox.common@2:true,org.eclipse.equinox.ds@1:true,org.eclipse.equinox.frameworkadmin.equinox@default:default,org.eclipse.equinox.frameworkadmin@default:default,org.eclipse.equinox.p2.artifact.repository@default:default,org.eclipse.equinox.p2.core@default:default,org.eclipse.equinox.p2.director@default:default,org.eclipse.equinox.p2.engine@default:default,org.eclipse.equinox.p2.garbagecollector@default:default,org.eclipse.equinox.p2.jarprocessor@default:default,org.eclipse.equinox.p2.metadata.repository@default:default,org.eclipse.equinox.p2.metadata@default:default,org.eclipse.equinox.p2.operations@default:default,org.eclipse.equinox.p2.publisher.eclipse@default:default,org.eclipse.equinox.p2.publisher@default:default,org.eclipse.equinox.p2.repository.tools@default:default,org.eclipse.equinox.p2.repository@default:default,org.eclipse.equinox.p2.touchpoint.eclipse@default:default,org.eclipse.equinox.p2.touchpoint.natives@default:default,org.eclipse.equinox.p2.transport.ecf@default:default,org.eclipse.equinox.preferences@default:default,org.eclipse.equinox.registry@default:default,org.eclipse.equinox.security.win32.x86_64@default:false,org.eclipse.equinox.security@default:default,org.eclipse.equinox.simpleconfigurator.manipulator@default:default,org.eclipse.equinox.simpleconfigurator@1:true,org.eclipse.equinox.util@default:default,org.eclipse.help@default:default,org.eclipse.jface.databinding@default:default,org.eclipse.jface@default:default,org.eclipse.osgi.compatibility.state@default:false,org.eclipse.osgi.services@default:default,org.eclipse.osgi@-1:true,org.eclipse.swt.win32.win32.x86_64@default:false,org.eclipse.swt@default:default,org.eclipse.team.core@default:default,org.eclipse.ui.trace@default:default,org.eclipse.ui.workbench@default:default,org.eclipse.ui@default:default,org.hamcrest.core@default:default,org.junit@default:default,org.sat4j.core@default:default,org.sat4j.pb@default:default,org.tukaani.xz@default:default,org.w3c.css.sac@default:default,org.w3c.dom.events@default:default,org.w3c.dom.smil@default:default,org.w3c.dom.svg@default:default"/>
+<stringAttribute key="selected_workspace_plugins" value="org.eclipse.oomph.base@default:default,org.eclipse.oomph.p2.core@default:default,org.eclipse.oomph.p2.tests@default:default,org.eclipse.oomph.p2@default:default,org.eclipse.oomph.tests@default:default,org.eclipse.oomph.util@default:default"/>
 <booleanAttribute key="show_selected_only" value="false"/>
 <stringAttribute key="templateConfig" value="${target_home}\configuration\config.ini"/>
 <booleanAttribute key="tracing" value="false"/>
diff --git a/plugins/org.eclipse.oomph.p2.tests/META-INF/MANIFEST.MF b/plugins/org.eclipse.oomph.p2.tests/META-INF/MANIFEST.MF
index b6b37e0..a64dfcf 100644
--- a/plugins/org.eclipse.oomph.p2.tests/META-INF/MANIFEST.MF
+++ b/plugins/org.eclipse.oomph.p2.tests/META-INF/MANIFEST.MF
@@ -2,12 +2,12 @@
 Bundle-ManifestVersion: 2
 Bundle-Name: %pluginName
 Bundle-SymbolicName: org.eclipse.oomph.p2.tests;singleton:=true
-Bundle-Version: 1.1.0.qualifier
+Bundle-Version: 1.2.0.qualifier
 Bundle-ClassPath: .
 Bundle-Vendor: %providerName
 Bundle-Localization: plugin
 Bundle-RequiredExecutionEnvironment: J2SE-1.5
-Export-Package: org.eclipse.oomph.p2.tests;version="1.1.0";x-internal:=true
+Export-Package: org.eclipse.oomph.p2.tests;version="1.2.0";x-internal:=true
 Require-Bundle: org.eclipse.equinox.p2.core;bundle-version="[2.0.0,3.0.0)";visibility:=reexport,
  org.eclipse.equinox.p2.engine;bundle-version="[2.0.0,3.0.0)";visibility:=reexport,
  org.eclipse.equinox.p2.metadata;bundle-version="[2.0.0,3.0.0)";visibility:=reexport,
diff --git a/plugins/org.eclipse.oomph.p2.tests/TransportTests.launch b/plugins/org.eclipse.oomph.p2.tests/TransportTests.launch
new file mode 100644
index 0000000..a8d0bd3
--- /dev/null
+++ b/plugins/org.eclipse.oomph.p2.tests/TransportTests.launch
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<launchConfiguration type="org.eclipse.pde.ui.JunitLaunchConfig">
+<booleanAttribute key="append.args" value="true"/>
+<stringAttribute key="application" value="org.eclipse.pde.junit.runtime.coretestapplication"/>
+<booleanAttribute key="askclear" value="false"/>
+<booleanAttribute key="automaticAdd" value="false"/>
+<booleanAttribute key="automaticValidate" value="true"/>
+<stringAttribute key="bootstrap" value=""/>
+<stringAttribute key="checked" value="[NONE]"/>
+<booleanAttribute key="clearConfig" value="true"/>
+<booleanAttribute key="clearws" value="true"/>
+<booleanAttribute key="clearwslog" value="false"/>
+<stringAttribute key="configLocation" value="${workspace_loc}/.metadata/.plugins/org.eclipse.pde.core/pde-junit"/>
+<booleanAttribute key="default" value="false"/>
+<booleanAttribute key="includeOptional" value="false"/>
+<stringAttribute key="location" value="${workspace_loc}/../junit-workspace"/>
+<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_PATHS">
+<listEntry value="/org.eclipse.oomph.p2.tests/src/org/eclipse/oomph/p2/tests/TransportTests.java"/>
+</listAttribute>
+<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_TYPES">
+<listEntry value="1"/>
+</listAttribute>
+<stringAttribute key="org.eclipse.jdt.junit.CONTAINER" value=""/>
+<booleanAttribute key="org.eclipse.jdt.junit.KEEPRUNNING_ATTR" value="false"/>
+<stringAttribute key="org.eclipse.jdt.junit.TESTNAME" value=""/>
+<stringAttribute key="org.eclipse.jdt.junit.TEST_KIND" value="org.eclipse.jdt.junit.loader.junit4"/>
+<booleanAttribute key="org.eclipse.jdt.launching.ATTR_USE_START_ON_FIRST_THREAD" value="true"/>
+<stringAttribute key="org.eclipse.jdt.launching.JRE_CONTAINER" value="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.6"/>
+<stringAttribute key="org.eclipse.jdt.launching.MAIN_TYPE" value="org.eclipse.oomph.p2.tests.TransportTests"/>
+<stringAttribute key="org.eclipse.jdt.launching.PROGRAM_ARGUMENTS" value="-os ${target.os} -ws ${target.ws} -arch ${target.arch} -nl ${target.nl} -consoleLog"/>
+<stringAttribute key="org.eclipse.jdt.launching.PROJECT_ATTR" value="org.eclipse.oomph.p2.tests"/>
+<stringAttribute key="org.eclipse.jdt.launching.SOURCE_PATH_PROVIDER" value="org.eclipse.pde.ui.workbenchClasspathProvider"/>
+<stringAttribute key="pde.version" value="3.3"/>
+<stringAttribute key="product" value="org.eclipse.equinox.p2.director.app.product"/>
+<booleanAttribute key="run_in_ui_thread" value="true"/>
+<stringAttribute key="selected_target_plugins" value="com.ibm.icu@default:default,javax.annotation@default:default,javax.inject@default:default,javax.xml@default:default,org.apache.batik.css@default:default,org.apache.batik.util.gui@default:default,org.apache.batik.util@default:default,org.apache.commons.codec@default:default,org.apache.commons.jxpath@default:default,org.apache.commons.logging@default:default,org.apache.httpcomponents.httpclient@default:default,org.apache.httpcomponents.httpcore@default:default,org.eclipse.ant.core@default:default,org.eclipse.compare.core@default:default,org.eclipse.core.commands@default:default,org.eclipse.core.contenttype@default:default,org.eclipse.core.databinding.observable@default:default,org.eclipse.core.databinding.property@default:default,org.eclipse.core.databinding@default:default,org.eclipse.core.expressions@default:default,org.eclipse.core.filesystem.java7@default:false,org.eclipse.core.filesystem.win32.x86_64@default:false,org.eclipse.core.filesystem@default:default,org.eclipse.core.jobs@default:default,org.eclipse.core.net.win32.x86_64@default:false,org.eclipse.core.net@default:default,org.eclipse.core.resources.win32.x86_64@default:false,org.eclipse.core.resources@default:default,org.eclipse.core.runtime.compatibility.registry@default:false,org.eclipse.core.runtime@default:true,org.eclipse.core.variables@default:default,org.eclipse.e4.core.commands@default:default,org.eclipse.e4.core.contexts@default:default,org.eclipse.e4.core.di.annotations@default:default,org.eclipse.e4.core.di.extensions@default:default,org.eclipse.e4.core.di@default:default,org.eclipse.e4.core.services@default:default,org.eclipse.e4.emf.xpath@default:default,org.eclipse.e4.ui.bindings@default:default,org.eclipse.e4.ui.css.core@default:default,org.eclipse.e4.ui.css.swt.theme@default:default,org.eclipse.e4.ui.css.swt@default:default,org.eclipse.e4.ui.di@default:default,org.eclipse.e4.ui.model.workbench@default:default,org.eclipse.e4.ui.services@default:default,org.eclipse.e4.ui.widgets@default:default,org.eclipse.e4.ui.workbench.addons.swt@default:default,org.eclipse.e4.ui.workbench.renderers.swt@default:default,org.eclipse.e4.ui.workbench.swt@default:default,org.eclipse.e4.ui.workbench3@default:default,org.eclipse.e4.ui.workbench@default:default,org.eclipse.ecf.filetransfer@default:default,org.eclipse.ecf.identity@default:default,org.eclipse.ecf.provider.filetransfer.httpclient4.ssl@default:false,org.eclipse.ecf.provider.filetransfer.httpclient4@default:default,org.eclipse.ecf.provider.filetransfer.ssl@default:false,org.eclipse.ecf.provider.filetransfer@default:default,org.eclipse.ecf.ssl@default:false,org.eclipse.ecf@default:default,org.eclipse.emf.common@default:default,org.eclipse.emf.ecore.change@default:default,org.eclipse.emf.ecore.xmi@default:default,org.eclipse.emf.ecore@default:default,org.eclipse.equinox.app@default:default,org.eclipse.equinox.common@2:true,org.eclipse.equinox.ds@1:true,org.eclipse.equinox.frameworkadmin.equinox@default:default,org.eclipse.equinox.frameworkadmin@default:default,org.eclipse.equinox.p2.artifact.repository@default:default,org.eclipse.equinox.p2.core@default:default,org.eclipse.equinox.p2.director@default:default,org.eclipse.equinox.p2.engine@default:default,org.eclipse.equinox.p2.garbagecollector@default:default,org.eclipse.equinox.p2.jarprocessor@default:default,org.eclipse.equinox.p2.metadata.repository@default:default,org.eclipse.equinox.p2.metadata@default:default,org.eclipse.equinox.p2.operations@default:default,org.eclipse.equinox.p2.publisher.eclipse@default:default,org.eclipse.equinox.p2.publisher@default:default,org.eclipse.equinox.p2.repository.tools@default:default,org.eclipse.equinox.p2.repository@default:default,org.eclipse.equinox.p2.touchpoint.eclipse@default:default,org.eclipse.equinox.p2.touchpoint.natives@default:default,org.eclipse.equinox.p2.transport.ecf@default:default,org.eclipse.equinox.preferences@default:default,org.eclipse.equinox.registry@default:default,org.eclipse.equinox.security.win32.x86_64@default:false,org.eclipse.equinox.security@default:default,org.eclipse.equinox.simpleconfigurator.manipulator@default:default,org.eclipse.equinox.simpleconfigurator@1:true,org.eclipse.equinox.util@default:default,org.eclipse.help@default:default,org.eclipse.jface.databinding@default:default,org.eclipse.jface@default:default,org.eclipse.osgi.compatibility.state@default:false,org.eclipse.osgi.services@default:default,org.eclipse.osgi@-1:true,org.eclipse.swt.win32.win32.x86_64@default:false,org.eclipse.swt@default:default,org.eclipse.team.core@default:default,org.eclipse.ui.trace@default:default,org.eclipse.ui.workbench@default:default,org.eclipse.ui@default:default,org.hamcrest.core@default:default,org.junit@default:default,org.sat4j.core@default:default,org.sat4j.pb@default:default,org.tukaani.xz@default:default,org.w3c.css.sac@default:default,org.w3c.dom.events@default:default,org.w3c.dom.smil@default:default,org.w3c.dom.svg@default:default"/>
+<stringAttribute key="selected_workspace_plugins" value="org.eclipse.oomph.base@default:default,org.eclipse.oomph.p2.core@default:default,org.eclipse.oomph.p2.tests@default:default,org.eclipse.oomph.p2@default:default,org.eclipse.oomph.tests@default:default,org.eclipse.oomph.util@default:default"/>
+<booleanAttribute key="show_selected_only" value="false"/>
+<stringAttribute key="templateConfig" value="${target_home}\configuration\config.ini"/>
+<booleanAttribute key="tracing" value="false"/>
+<booleanAttribute key="useCustomFeatures" value="false"/>
+<booleanAttribute key="useDefaultConfig" value="true"/>
+<booleanAttribute key="useDefaultConfigArea" value="false"/>
+<booleanAttribute key="useProduct" value="false"/>
+</launchConfiguration>
diff --git a/plugins/org.eclipse.oomph.p2.tests/pom.xml b/plugins/org.eclipse.oomph.p2.tests/pom.xml
index 63f12e2..0c3e9f8 100644
--- a/plugins/org.eclipse.oomph.p2.tests/pom.xml
+++ b/plugins/org.eclipse.oomph.p2.tests/pom.xml
@@ -20,7 +20,7 @@
   </parent>
   <groupId>org.eclipse.oomph</groupId>
   <artifactId>org.eclipse.oomph.p2.tests</artifactId>
-  <version>1.1.0-SNAPSHOT</version>
+  <version>1.2.0-SNAPSHOT</version>
   <packaging>eclipse-plugin</packaging>
 
   <build>
diff --git a/plugins/org.eclipse.oomph.p2.tests/src/org/eclipse/oomph/p2/tests/AbstractP2Test.java b/plugins/org.eclipse.oomph.p2.tests/src/org/eclipse/oomph/p2/tests/AbstractP2Test.java
index 3e59a21..27a2a06 100644
--- a/plugins/org.eclipse.oomph.p2.tests/src/org/eclipse/oomph/p2/tests/AbstractP2Test.java
+++ b/plugins/org.eclipse.oomph.p2.tests/src/org/eclipse/oomph/p2/tests/AbstractP2Test.java
@@ -10,9 +10,6 @@
  */
 package org.eclipse.oomph.p2.tests;
 
-import static org.hamcrest.CoreMatchers.is;
-import static org.hamcrest.MatcherAssert.assertThat;
-
 import org.eclipse.oomph.p2.core.Agent;
 import org.eclipse.oomph.p2.core.AgentManager;
 import org.eclipse.oomph.p2.core.P2Util;
@@ -52,7 +49,7 @@
 @SuppressWarnings("restriction")
 public abstract class AbstractP2Test extends AbstractTest
 {
-  private static final String TMP = PropertiesUtil.getProperty("java.io.tmpdir");
+  protected static final String TMP = PropertiesUtil.getProperty("java.io.tmpdir");
 
   private static final String CDO = "p2-test-mirror-001-cdo";
 
@@ -62,6 +59,96 @@
 
   private static final String SIMPLE_ARTIFACT_REPOSITORY = IArtifactRepositoryManager.TYPE_SIMPLE_REPOSITORY;
 
+  private static final VersionedIdFilter CDO_FILTER = new VersionedIdFilter()
+  {
+    public boolean matches(IVersionedId versionedId)
+    {
+      String id = versionedId.getId();
+      return id.startsWith("org.eclipse.net4j.util") || id.startsWith("org.apache");
+    }
+  };
+
+  private static final VersionedIdFilter PLATFORM_FILTER = new VersionedIdFilter()
+  {
+    public boolean matches(IVersionedId versionedId)
+    {
+      String id = versionedId.getId();
+      return id.startsWith("com.jcraft.jsch") || id.startsWith("org.apache") || id.startsWith("a.jre");
+    }
+  };
+
+  public static final File CDO_OLD = new File(TMP, CDO + "-old");
+
+  public static final File CDO_NEW = new File(TMP, CDO + "-new");
+
+  public static final File PLATFORM_OLD = new File(TMP, PLATFORM + "-old");
+
+  public static final File PLATFORM_NEW = new File(TMP, PLATFORM + "-new");
+
+  @Override
+  public void setUp() throws Exception
+  {
+    super.setUp();
+    AgentManagerImpl.instance = new AgentManagerImpl(getUserHome());
+  }
+
+  @Override
+  public void tearDown() throws Exception
+  {
+    AgentManagerImpl.instance = null;
+    super.tearDown();
+  }
+
+  protected Agent getAgent()
+  {
+    AgentManager agentManager = P2Util.getAgentManager();
+    return agentManager.getAgents().iterator().next();
+  }
+
+  protected Agent getFreshAgent()
+  {
+    AgentManagerImpl.instance = new AgentManagerImpl(getUserHome());
+    return getAgent();
+  }
+
+  protected void commitProfileTransaction(ProfileTransaction transaction, boolean expectedChange) throws CoreException
+  {
+    commitProfileTransaction(transaction, expectedChange, LOGGER);
+  }
+
+  protected void commitProfileTransaction(ProfileTransaction transaction, boolean expectedChange, IProgressMonitor monitor) throws CoreException
+  {
+    boolean actualChange = transaction.commit(monitor);
+    if (actualChange != expectedChange)
+    {
+      if (expectedChange)
+      {
+        throw new AssertionError("Profile should have changed, but didn't");
+      }
+
+      throw new AssertionError("Profile should not have changed, but did");
+    }
+  }
+
+  @BeforeClass
+  public static void setUpBeforeClass() throws Exception
+  {
+    mirror("http://download.eclipse.org/modeling/emf/cdo/drops/R20130918-0029", CDO_OLD, CDO_FILTER);
+    mirror("http://download.eclipse.org/modeling/emf/cdo/drops/R20140218-1655", CDO_NEW, CDO_FILTER);
+    mirror("http://download.eclipse.org/eclipse/updates/4.3/R-4.3.1-201309111000", PLATFORM_OLD, PLATFORM_FILTER);
+    mirror("http://download.eclipse.org/eclipse/updates/4.3/R-4.3.2-201402211700", PLATFORM_NEW, PLATFORM_FILTER);
+  }
+
+  private static void mirror(String repo, File local, VersionedIdFilter filter) throws Exception
+  {
+    if (!local.isDirectory())
+    {
+      LOGGER.setTaskName("Creating test mirror of " + repo + " under " + local);
+      mirrorRepository(new URI(repo), local.toURI(), filter, LOGGER);
+      LOGGER.setTaskName(null);
+    }
+  }
+
   public static void mirrorArtifactRepository(URI sourceURI, URI targetURI, VersionedIdFilter filter, IProgressMonitor monitor) throws CoreException
   {
     List<URI> repositoriesToRemove = new ArrayList<URI>();
@@ -163,81 +250,4 @@
       }
     }
   }
-
-  private static final VersionedIdFilter CDO_FILTER = new VersionedIdFilter()
-  {
-    public boolean matches(IVersionedId versionedId)
-    {
-      String id = versionedId.getId();
-      return id.startsWith("org.eclipse.net4j.util") || id.startsWith("org.apache");
-    }
-  };
-
-  private static final VersionedIdFilter PLATFORM_FILTER = new VersionedIdFilter()
-  {
-    public boolean matches(IVersionedId versionedId)
-    {
-      String id = versionedId.getId();
-      return id.startsWith("com.jcraft.jsch") || id.startsWith("org.apache") || id.startsWith("a.jre");
-    }
-  };
-
-  public static final File CDO_OLD = new File(TMP, CDO + "-old");
-
-  public static final File CDO_NEW = new File(TMP, CDO + "-new");
-
-  public static final File PLATFORM_OLD = new File(TMP, PLATFORM + "-old");
-
-  public static final File PLATFORM_NEW = new File(TMP, PLATFORM + "-new");
-
-  @BeforeClass
-  public static void setUpBeforeClass() throws Exception
-  {
-    mirror("http://download.eclipse.org/modeling/emf/cdo/drops/R20130918-0029", CDO_OLD, CDO_FILTER);
-    mirror("http://download.eclipse.org/modeling/emf/cdo/drops/R20140218-1655", CDO_NEW, CDO_FILTER);
-    mirror("http://download.eclipse.org/eclipse/updates/4.3/R-4.3.1-201309111000", PLATFORM_OLD, PLATFORM_FILTER);
-    mirror("http://download.eclipse.org/eclipse/updates/4.3/R-4.3.2-201402211700", PLATFORM_NEW, PLATFORM_FILTER);
-  }
-
-  private static void mirror(String repo, File local, VersionedIdFilter filter) throws Exception
-  {
-    if (!local.isDirectory())
-    {
-      LOGGER.setTaskName("Creating test mirror of " + repo + " under " + local);
-      mirrorRepository(new URI(repo), local.toURI(), filter, LOGGER);
-      LOGGER.setTaskName(null);
-    }
-  }
-
-  @Override
-  public void setUp() throws Exception
-  {
-    super.setUp();
-    AgentManagerImpl.instance = new AgentManagerImpl(getUserHome());
-  }
-
-  @Override
-  public void tearDown() throws Exception
-  {
-    AgentManagerImpl.instance = null;
-    super.tearDown();
-  }
-
-  protected Agent getAgent()
-  {
-    AgentManager agentManager = P2Util.getAgentManager();
-    return agentManager.getAgents().iterator().next();
-  }
-
-  protected Agent getFreshAgent()
-  {
-    AgentManagerImpl.instance = new AgentManagerImpl(getUserHome());
-    return getAgent();
-  }
-
-  protected void commitProfileTransaction(ProfileTransaction transaction, boolean expectedChange) throws CoreException
-  {
-    boolean actualChange = transaction.commit(LOGGER);
-    assertThat(actualChange, is(expectedChange));
-  }
 }
diff --git a/plugins/org.eclipse.oomph.p2.tests/src/org/eclipse/oomph/p2/tests/AgentTests.java b/plugins/org.eclipse.oomph.p2.tests/src/org/eclipse/oomph/p2/tests/AgentTests.java
index bb9c6f4..3904c2a 100644
--- a/plugins/org.eclipse.oomph.p2.tests/src/org/eclipse/oomph/p2/tests/AgentTests.java
+++ b/plugins/org.eclipse.oomph.p2.tests/src/org/eclipse/oomph/p2/tests/AgentTests.java
@@ -26,22 +26,39 @@
 import org.eclipse.oomph.p2.core.ProfileTransaction;
 import org.eclipse.oomph.p2.core.ProfileTransaction.Resolution;
 import org.eclipse.oomph.p2.internal.core.AgentManagerImpl;
+import org.eclipse.oomph.p2.internal.core.CachingRepositoryManager;
 import org.eclipse.oomph.p2.internal.core.ProfileImpl;
 import org.eclipse.oomph.util.IOUtil;
 
 import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.NullProgressMonitor;
+import org.eclipse.core.runtime.URIUtil;
 import org.eclipse.equinox.p2.engine.IProfile;
 import org.eclipse.equinox.p2.engine.IProfileRegistry;
+import org.eclipse.equinox.p2.metadata.VersionRange;
 
 import org.junit.Assert;
+import org.junit.FixMethodOrder;
 import org.junit.Test;
+import org.junit.runners.MethodSorters;
 
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
 import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URL;
 import java.util.Arrays;
 
 /**
  * @author Eike Stepper
  */
+@FixMethodOrder(MethodSorters.JVM)
 public class AgentTests extends AbstractP2Test
 {
   @Test
@@ -259,7 +276,7 @@
 
     // No update (keep new version)
     ProfileTransaction transaction3 = profile.change().setRemoveExistingInstallableUnits(true);
-    commitProfileTransaction(transaction3, true);
+    commitProfileTransaction(transaction3, false);
     assertThat(features.list().length, is(1));
     assertThat(new File(features, newVersion).isDirectory(), is(true));
   }
@@ -304,7 +321,7 @@
 
     // No update (keep new version)
     ProfileTransaction transaction3 = profile.change().setRemoveExistingInstallableUnits(true);
-    commitProfileTransaction(transaction3, true);
+    commitProfileTransaction(transaction3, false);
     assertThat(plugins.list().length, is(1));
     assertThat(new File(plugins, newVersion).isFile(), is(true));
   }
@@ -324,7 +341,8 @@
     // Install
     ProfileTransaction transaction1 = profile.change();
     ProfileDefinition profileDefinition = transaction1.getProfileDefinition();
-    profileDefinition.getRequirements().add(P2Factory.eINSTANCE.createRequirement("com.jcraft.jsch"));
+    profileDefinition.getRequirements()
+        .add(P2Factory.eINSTANCE.createRequirement("com.jcraft.jsch", new VersionRange("[0.1.46.v201205102330,0.1.46.v201205102330]")));
     profileDefinition.getRepositories().add(P2Factory.eINSTANCE.createRepository(PLATFORM_OLD.toURI().toString()));
 
     commitProfileTransaction(transaction1, true);
@@ -336,21 +354,26 @@
     File plugins = new File(installFolder, "plugins");
     assertThat(plugins.list().length, is(1));
     assertThat(new File(plugins, oldVersion).isFile(), is(true));
+    assertThat(new File(plugins, newVersion).isFile(), is(false));
 
-    // Update (add new version)
+    // Update (keep old version)
     ProfileTransaction transaction2 = profile.change();
     transaction2.getProfileDefinition().getRepositories().add(P2Factory.eINSTANCE.createRepository(PLATFORM_NEW.toURI().toString()));
 
-    commitProfileTransaction(transaction2, true);
+    commitProfileTransaction(transaction2, true); // Profile must change, but only the profileDefinition property.
+    assertThat(plugins.list().length, is(1));
+    assertThat(new File(plugins, oldVersion).isFile(), is(true));
+    assertThat(new File(plugins, newVersion).isFile(), is(false));
+
+    // Update (add new version)
+    ProfileTransaction transaction3 = profile.change().setRemoveExistingInstallableUnits(true);
+    transaction3.getProfileDefinition().getRequirements()
+        .add(P2Factory.eINSTANCE.createRequirement("com.jcraft.jsch", new VersionRange("[0.1.50.v201310081430,0.1.50.v201310081430]")));
+
+    commitProfileTransaction(transaction3, true);
     assertThat(plugins.list().length, is(2));
     assertThat(new File(plugins, oldVersion).isFile(), is(true));
     assertThat(new File(plugins, newVersion).isFile(), is(true));
-
-    // Update (remove old version)
-    ProfileTransaction transaction3 = profile.change().setRemoveExistingInstallableUnits(true);
-    commitProfileTransaction(transaction3, true);
-    assertThat(plugins.list().length, is(1));
-    assertThat(new File(plugins, newVersion).isFile(), is(true));
   }
 
   @Test
@@ -430,4 +453,117 @@
     File artifacts = new File(installFolder, "artifacts.xml");
     assertThat(artifacts.isFile(), is(true));
   }
+
+  @Test
+  public void testP2IndexSorting() throws Exception
+  {
+    final Agent testAgent = getAgent();
+
+    @SuppressWarnings("restriction")
+    class RepoMan extends CachingRepositoryManager.Metadata
+    {
+      private final String INDEX_FILE = "p2.index"; //$NON-NLS-1$
+
+      public RepoMan()
+      {
+        super(testAgent.getProvisioningAgent(), null);
+      }
+
+      public void test(String uri) throws Exception
+      {
+        URI location = new URI(uri);
+        org.eclipse.equinox.internal.p2.repository.helpers.LocationProperties indexFile = loadIndexFile(location, new NullProgressMonitor());
+
+        String[] allSuffixes = getAllSuffixes();
+        System.out.println("allSuffixes: " + Arrays.asList(allSuffixes));
+
+        String[] preferredOrder = getPreferredRepositorySearchOrder(indexFile);
+        System.out.println("preferredOrder: " + Arrays.asList(preferredOrder));
+
+        String[] suffixes = sortSuffixes(allSuffixes, location, preferredOrder);
+        System.out.println("suffixes: " + Arrays.asList(suffixes));
+      }
+
+      /**
+       * Fetches the p2.index file from the server. If the file could not be fetched
+       * a NullSafe version is returned.
+       */
+      private org.eclipse.equinox.internal.p2.repository.helpers.LocationProperties loadIndexFile(URI location, IProgressMonitor monitor)
+      {
+        org.eclipse.equinox.internal.p2.repository.helpers.LocationProperties locationProperties = org.eclipse.equinox.internal.p2.repository.helpers.LocationProperties
+            .createEmptyIndexFile();
+        // Handle the case of in-memory repos
+        if (!isURL(location))
+        {
+          return locationProperties;
+        }
+
+        if ("file".equals(location.getScheme())) //$NON-NLS-1$
+        {
+          InputStream localStream = null;
+          try
+          {
+            try
+            {
+              File indexFile = URIUtil.toFile(getIndexFileURI(location));
+              if (indexFile != null && indexFile.exists() && indexFile.canRead())
+              {
+                localStream = new FileInputStream(indexFile);
+                locationProperties = org.eclipse.equinox.internal.p2.repository.helpers.LocationProperties.create(localStream);
+              }
+            }
+            finally
+            {
+              if (localStream != null)
+              {
+                localStream.close();
+              }
+            }
+          }
+          catch (IOException e)
+          {
+            // do nothing.
+          }
+          return locationProperties;
+        }
+
+        // Handle non local repos (i.e. not file:)
+        ByteArrayOutputStream index = new ByteArrayOutputStream();
+        IStatus indexFileStatus = null;
+        indexFileStatus = getTransport().download(getIndexFileURI(location), index, monitor);
+        if (indexFileStatus != null && indexFileStatus.isOK())
+        {
+          locationProperties = org.eclipse.equinox.internal.p2.repository.helpers.LocationProperties.create(new ByteArrayInputStream(index.toByteArray()));
+        }
+
+        return locationProperties;
+      }
+
+      private boolean isURL(URI location)
+      {
+        try
+        {
+          new URL(location.toASCIIString());
+        }
+        catch (MalformedURLException e)
+        {
+          return false;
+        }
+        return true;
+      }
+
+      private URI getIndexFileURI(URI base)
+      {
+        final String name = INDEX_FILE;
+        String spec = base.toString();
+        if (spec.endsWith(name))
+        {
+          return base;
+        }
+        return URIUtil.append(base, name);
+      }
+    }
+
+    new RepoMan().test("http://download.eclipse.org/releases/mars");
+  }
 }
diff --git a/plugins/org.eclipse.oomph.p2.tests/src/org/eclipse/oomph/p2/tests/TransportTests.java b/plugins/org.eclipse.oomph.p2.tests/src/org/eclipse/oomph/p2/tests/TransportTests.java
new file mode 100644
index 0000000..cd5ee39
--- /dev/null
+++ b/plugins/org.eclipse.oomph.p2.tests/src/org/eclipse/oomph/p2/tests/TransportTests.java
@@ -0,0 +1,683 @@
+/*
+ * Copyright (c) 2014, 2015 Eike Stepper (Berlin, Germany) and others.
+ * 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:
+ *    Eike Stepper - initial API and implementation
+ */
+package org.eclipse.oomph.p2.tests;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import org.eclipse.oomph.internal.util.HTTPServer;
+import org.eclipse.oomph.internal.util.HTTPServer.Context;
+import org.eclipse.oomph.internal.util.HTTPServer.FileContext;
+import org.eclipse.oomph.p2.P2Factory;
+import org.eclipse.oomph.p2.ProfileDefinition;
+import org.eclipse.oomph.p2.core.Agent;
+import org.eclipse.oomph.p2.core.AgentManager;
+import org.eclipse.oomph.p2.core.P2Util;
+import org.eclipse.oomph.p2.core.Profile;
+import org.eclipse.oomph.p2.core.ProfileTransaction;
+import org.eclipse.oomph.p2.internal.core.CachingTransport;
+import org.eclipse.oomph.util.IOUtil;
+import org.eclipse.oomph.util.PropertiesUtil;
+
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.NullProgressMonitor;
+
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.FixMethodOrder;
+import org.junit.Test;
+import org.junit.runners.MethodSorters;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @author Eike Stepper
+ */
+@FixMethodOrder(MethodSorters.JVM)
+public class TransportTests extends AbstractP2Test
+{
+  private static final boolean ASSERT_REQUESTS = true;
+
+  private static HTTPServer httpServer;
+
+  private List<Context> contexts = new ArrayList<Context>();
+
+  @BeforeClass
+  public static void setUpBeforeClass() throws Exception
+  {
+    httpServer = new HTTPServer(2345, 5000);
+  }
+
+  @AfterClass
+  public static void tearDownAfterClass() throws Exception
+  {
+    if (httpServer != null)
+    {
+      httpServer.stop();
+      httpServer = null;
+    }
+  }
+
+  @Override
+  public void tearDown() throws Exception
+  {
+    for (Context context : contexts)
+    {
+      httpServer.removeContext(context);
+    }
+
+    super.tearDown();
+  }
+
+  private FileContext addRepo(String path, File folder, boolean composite, boolean p2Index) throws Exception
+  {
+    if (composite)
+    {
+      IOUtil.copyTree(CDO_NEW, new File(folder, "child"));
+
+      IOUtil.writeUTF8(new File(folder, "compositeContent.xml"), "<?xml version='1.0' encoding='UTF-8'?>\n" //
+          + "<?compositeMetadataRepository version='1.0.0'?>\n" //
+          + "<repository name='Eclipse Mars repository' type='org.eclipse.equinox.internal.p2.metadata.repository.CompositeMetadataRepository' version='1.0.0'>\n" //
+          + "    <properties size='3'>\n" //
+          + "        <property name='p2.timestamp' value='1313779613760'/>\n" //
+          + "        <property name='p2.atomic.composite.loading' value='true'/>\n" //
+          + "    </properties>\n" //
+          + "    <children size='1'>\n" //
+          + "        <child location='child' />\n" //
+          + "    </children>    \n" //
+          + "</repository>\n");
+
+      IOUtil.writeUTF8(new File(folder, "compositeArtifacts.xml"), "<?xml version='1.0' encoding='UTF-8'?>\n" //
+          + "<?compositeArtifactRepository version='1.0.0'?>\n" //
+          + "<repository name='Eclipse Mars repository' type='org.eclipse.equinox.internal.p2.artifact.repository.CompositeArtifactRepository' version='1.0.0'>\n" //
+          + "    <properties size='3'>\n" //
+          + "        <property name='p2.timestamp' value='1313779613118'/>\n" //
+          + "        <property name='p2.atomic.composite.loading' value='true'/>\n" //
+          + "    </properties>\n" //
+          + "    <children size='1'>\n" //
+          + "        <child location='child' />\n" //
+          + "    </children>\n" //
+          + "</repository>\n");
+
+      if (p2Index)
+      {
+        IOUtil.writeUTF8(new File(folder, "p2.index"),
+            "version=1\n" //
+                + "metadata.repository.factory.order=compositeContent.xml,\\!\n" //
+                + "artifact.repository.factory.order=compositeArtifacts.xml,\\!\n");
+      }
+    }
+    else
+    {
+      IOUtil.copyTree(CDO_NEW, folder);
+
+      if (p2Index)
+      {
+        IOUtil.writeUTF8(new File(folder, "p2.index"),
+            "version=1\n" //
+                + "metadata.repository.factory.order=content.xml,\\!\n" //
+                + "artifact.repository.factory.order=artifacts.xml,\\!\n");
+      }
+    }
+
+    FileContext context = new FileContext(path, false, folder);
+    httpServer.addContext(context);
+    contexts.add(context);
+    return context;
+  }
+
+  private void removeRepo(FileContext context)
+  {
+    httpServer.removeContext(context);
+    IOUtil.deleteBestEffort(context.getRoot());
+  }
+
+  private String removePort(String request)
+  {
+    return request.replace(":" + httpServer.getPort() + "/", "/");
+  }
+
+  private List<String> install(int installation) throws Exception
+  {
+    String url = httpServer + "/cdo";
+    File installFolder = new File(getUserHome(), "app" + installation);
+
+    AgentManager agentManager = P2Util.getAgentManager();
+    Agent agent = agentManager.addAgent(new File(installFolder, "p2"));
+
+    CachingTransport cachingTransport = (CachingTransport)agent.getProvisioningAgent().getService(CachingTransport.SERVICE_NAME);
+
+    CountingTransport countingTransport = new CountingTransport(cachingTransport.getDelegate());
+    cachingTransport.setDelegate(countingTransport);
+
+    Profile profile = agent.addProfile("profile-app" + installation, "Installation").setCacheFolder(installFolder).setInstallFolder(installFolder).create();
+    ProfileTransaction transaction = profile.change();
+    transaction.setMirrors(false);
+
+    ProfileDefinition profileDefinition = transaction.getProfileDefinition();
+    profileDefinition.getRequirements().add(P2Factory.eINSTANCE.createRequirement("org.eclipse.net4j.util"));
+    profileDefinition.getRepositories().add(P2Factory.eINSTANCE.createRepository(url));
+
+    commitProfileTransaction(transaction, true, new NullProgressMonitor());
+    assertThat(installFolder.isDirectory(), is(true));
+    assertThat(installFolder.list().length, is(3));
+    assertThat(new File(installFolder, "p2").isDirectory(), is(true));
+    assertThat(new File(installFolder, "artifacts.xml").isFile(), is(true));
+
+    File plugins = new File(installFolder, "plugins");
+    assertThat(plugins.isDirectory(), is(true));
+    assertThat(plugins.list().length, is(1));
+
+    List<String> results = new ArrayList<String>();
+    results.addAll(countingTransport.getRequests());
+
+    File cacheFile = cachingTransport.getCacheFile(new URI(url + "/p2.index"));
+    Map<String, String> p2Index = PropertiesUtil.loadProperties(cacheFile);
+    for (Map.Entry<String, String> entry : p2Index.entrySet())
+    {
+      results.add("     " + entry.getKey() + "=" + entry.getValue());
+    }
+
+    return results;
+  }
+
+  private List<String> test(boolean startWithComposite, boolean startWithP2Index, boolean endWithP2Index) throws Exception
+  {
+    List<String> results = new ArrayList<String>();
+
+    File root = new File(getUserHome(), "cdo");
+    System.out.println(root);
+    System.out.println();
+
+    FileContext context1 = addRepo("/cdo", root, startWithComposite, startWithP2Index);
+
+    try
+    {
+      results.add("INSTALL 1");
+      results.addAll(install(1));
+    }
+    finally
+    {
+      removeRepo(context1);
+    }
+
+    FileContext context2 = addRepo("/cdo", root, !startWithComposite, endWithP2Index);
+
+    try
+    {
+      results.add("INSTALL 2");
+      results.addAll(install(2));
+    }
+    finally
+    {
+      removeRepo(context2);
+    }
+
+    return results;
+  }
+
+  private void assertResults(List<String> results, String... expectedResults)
+  {
+    System.out.println("assertResults(results //");
+    int i = 0;
+    boolean failed = false;
+
+    for (String result : results)
+    {
+      result = removePort(result);
+      System.out.print(", \"" + result + "\" //");
+
+      if (!ASSERT_REQUESTS)
+      {
+        System.out.println();
+        continue;
+      }
+
+      int index = i++;
+      if (index >= expectedResults.length)
+      {
+        System.out.println(" -------------------> UNEXPECTED");
+        failed = true;
+      }
+      else
+      {
+        String expectedResult = removePort(expectedResults[index]);
+        if (result.equals(expectedResult))
+        {
+          System.out.println();
+        }
+        else
+        {
+          System.out.println(" ------------------->  " + expectedResult);
+          failed = true;
+        }
+      }
+    }
+
+    System.out.println(");\n\n");
+
+    if (ASSERT_REQUESTS)
+    {
+      for (int j = i; j < expectedResults.length; j++)
+      {
+        System.out.println("MISSING: " + expectedResults[j]);
+        failed = true;
+      }
+
+      if (failed)
+      {
+        throw new AssertionError("Unexpected Results");
+      }
+    }
+  }
+
+  @Test
+  public void test_1_CompositeToSimpleWithoutIndex() throws Exception
+  {
+    List<String> results = test(true, false, false);
+    assertResults(results //
+        , "INSTALL 1" //
+        , "GET  http://127.0.0.1/cdo/p2.index" //
+        , "HEAD http://127.0.0.1/cdo/content.jar" //
+        , "HEAD http://127.0.0.1/cdo/content.xml" //
+        , "HEAD http://127.0.0.1/cdo/content.xml.xz" //
+        , "HEAD http://127.0.0.1/cdo/compositeContent.jar" //
+        , "HEAD http://127.0.0.1/cdo/compositeContent.xml" //
+        , "GET  http://127.0.0.1/cdo/compositeContent.xml" //
+        , "GET  http://127.0.0.1/cdo/child/p2.index" //
+        , "HEAD http://127.0.0.1/cdo/child/content.jar" //
+        , "GET  http://127.0.0.1/cdo/child/content.jar" //
+        , "HEAD http://127.0.0.1/cdo/artifacts.jar" //
+        , "HEAD http://127.0.0.1/cdo/artifacts.xml" //
+        , "HEAD http://127.0.0.1/cdo/artifacts.xml.xz" //
+        , "HEAD http://127.0.0.1/cdo/compositeArtifacts.jar" //
+        , "HEAD http://127.0.0.1/cdo/compositeArtifacts.xml" //
+        , "GET  http://127.0.0.1/cdo/compositeArtifacts.xml" //
+        , "HEAD http://127.0.0.1/cdo/child/artifacts.jar" //
+        , "GET  http://127.0.0.1/cdo/child/artifacts.jar" //
+        , "GET  http://127.0.0.1/cdo/child/plugins/org.eclipse.net4j.util_3.3.1.v20140218-1709.jar" //
+        , "     version=1" //
+        , "     metadata.repository.factory.order=compositeContent.xml" //
+        , "     artifact.repository.factory.order=compositeArtifacts.xml" //
+        , "INSTALL 2" //
+        , "HEAD http://127.0.0.1/cdo/compositeContent.jar" //
+        , "HEAD http://127.0.0.1/cdo/compositeContent.xml" //
+        , "HEAD http://127.0.0.1/cdo/content.jar" //
+        , "GET  http://127.0.0.1/cdo/content.jar" //
+        , "HEAD http://127.0.0.1/cdo/compositeArtifacts.jar" //
+        , "HEAD http://127.0.0.1/cdo/compositeArtifacts.xml" //
+        , "HEAD http://127.0.0.1/cdo/artifacts.jar" //
+        , "GET  http://127.0.0.1/cdo/artifacts.jar" //
+        , "GET  http://127.0.0.1/cdo/plugins/org.eclipse.net4j.util_3.3.1.v20140218-1709.jar" //
+        , "     version=1" //
+        , "     metadata.repository.factory.order=content.xml" //
+        , "     artifact.repository.factory.order=artifacts.xml" //
+    );
+  }
+
+  @Test
+  public void test_2_CompositeToSimpleRemoveIndex() throws Exception
+  {
+    List<String> results = test(true, true, false);
+    assertResults(results //
+        , "INSTALL 1" //
+        , "GET  http://127.0.0.1/cdo/p2.index" //
+        , "HEAD http://127.0.0.1/cdo/compositeContent.jar" //
+        , "HEAD http://127.0.0.1/cdo/compositeContent.xml" //
+        , "GET  http://127.0.0.1/cdo/compositeContent.xml" //
+        , "GET  http://127.0.0.1/cdo/child/p2.index" //
+        , "HEAD http://127.0.0.1/cdo/child/content.jar" //
+        , "GET  http://127.0.0.1/cdo/child/content.jar" //
+        , "HEAD http://127.0.0.1/cdo/compositeArtifacts.jar" //
+        , "HEAD http://127.0.0.1/cdo/compositeArtifacts.xml" //
+        , "GET  http://127.0.0.1/cdo/compositeArtifacts.xml" //
+        , "HEAD http://127.0.0.1/cdo/child/artifacts.jar" //
+        , "GET  http://127.0.0.1/cdo/child/artifacts.jar" //
+        , "GET  http://127.0.0.1/cdo/child/plugins/org.eclipse.net4j.util_3.3.1.v20140218-1709.jar" //
+        , "     version=1" //
+        , "     metadata.repository.factory.order=compositeContent.xml" //
+        , "     artifact.repository.factory.order=compositeArtifacts.xml" //
+        , "INSTALL 2" //
+        , "HEAD http://127.0.0.1/cdo/compositeContent.jar" //
+        , "HEAD http://127.0.0.1/cdo/compositeContent.xml" //
+        , "HEAD http://127.0.0.1/cdo/content.jar" //
+        , "GET  http://127.0.0.1/cdo/content.jar" //
+        , "HEAD http://127.0.0.1/cdo/compositeArtifacts.jar" //
+        , "HEAD http://127.0.0.1/cdo/compositeArtifacts.xml" //
+        , "HEAD http://127.0.0.1/cdo/artifacts.jar" //
+        , "GET  http://127.0.0.1/cdo/artifacts.jar" //
+        , "GET  http://127.0.0.1/cdo/plugins/org.eclipse.net4j.util_3.3.1.v20140218-1709.jar" //
+        , "     version=1" //
+        , "     metadata.repository.factory.order=content.xml" //
+        , "     artifact.repository.factory.order=artifacts.xml" //
+    );
+  }
+
+  @Test
+  public void test_3_CompositeToSimpleAddIndex() throws Exception
+  {
+    List<String> results = test(true, false, true);
+    assertResults(results //
+        , "INSTALL 1" //
+        , "GET  http://127.0.0.1/cdo/p2.index" //
+        , "HEAD http://127.0.0.1/cdo/content.jar" //
+        , "HEAD http://127.0.0.1/cdo/content.xml" //
+        , "HEAD http://127.0.0.1/cdo/content.xml.xz" //
+        , "HEAD http://127.0.0.1/cdo/compositeContent.jar" //
+        , "HEAD http://127.0.0.1/cdo/compositeContent.xml" //
+        , "GET  http://127.0.0.1/cdo/compositeContent.xml" //
+        , "GET  http://127.0.0.1/cdo/child/p2.index" //
+        , "HEAD http://127.0.0.1/cdo/child/content.jar" //
+        , "GET  http://127.0.0.1/cdo/child/content.jar" //
+        , "HEAD http://127.0.0.1/cdo/artifacts.jar" //
+        , "HEAD http://127.0.0.1/cdo/artifacts.xml" //
+        , "HEAD http://127.0.0.1/cdo/artifacts.xml.xz" //
+        , "HEAD http://127.0.0.1/cdo/compositeArtifacts.jar" //
+        , "HEAD http://127.0.0.1/cdo/compositeArtifacts.xml" //
+        , "GET  http://127.0.0.1/cdo/compositeArtifacts.xml" //
+        , "HEAD http://127.0.0.1/cdo/child/artifacts.jar" //
+        , "GET  http://127.0.0.1/cdo/child/artifacts.jar" //
+        , "GET  http://127.0.0.1/cdo/child/plugins/org.eclipse.net4j.util_3.3.1.v20140218-1709.jar" //
+        , "     version=1" //
+        , "     metadata.repository.factory.order=compositeContent.xml" //
+        , "     artifact.repository.factory.order=compositeArtifacts.xml" //
+        , "INSTALL 2" //
+        , "HEAD http://127.0.0.1/cdo/compositeContent.jar" //
+        , "HEAD http://127.0.0.1/cdo/compositeContent.xml" //
+        , "HEAD http://127.0.0.1/cdo/content.jar" //
+        , "GET  http://127.0.0.1/cdo/content.jar" //
+        , "HEAD http://127.0.0.1/cdo/compositeArtifacts.jar" //
+        , "HEAD http://127.0.0.1/cdo/compositeArtifacts.xml" //
+        , "HEAD http://127.0.0.1/cdo/artifacts.jar" //
+        , "GET  http://127.0.0.1/cdo/artifacts.jar" //
+        , "GET  http://127.0.0.1/cdo/plugins/org.eclipse.net4j.util_3.3.1.v20140218-1709.jar" //
+        , "     version=1" //
+        , "     metadata.repository.factory.order=content.xml" //
+        , "     artifact.repository.factory.order=artifacts.xml" //
+    );
+  }
+
+  @Test
+  public void test_4_CompositeToSimpleWithIndex() throws Exception
+  {
+    List<String> results = test(true, true, true);
+    assertResults(results //
+        , "INSTALL 1" //
+        , "GET  http://127.0.0.1/cdo/p2.index" //
+        , "HEAD http://127.0.0.1/cdo/compositeContent.jar" //
+        , "HEAD http://127.0.0.1/cdo/compositeContent.xml" //
+        , "GET  http://127.0.0.1/cdo/compositeContent.xml" //
+        , "GET  http://127.0.0.1/cdo/child/p2.index" //
+        , "HEAD http://127.0.0.1/cdo/child/content.jar" //
+        , "GET  http://127.0.0.1/cdo/child/content.jar" //
+        , "HEAD http://127.0.0.1/cdo/compositeArtifacts.jar" //
+        , "HEAD http://127.0.0.1/cdo/compositeArtifacts.xml" //
+        , "GET  http://127.0.0.1/cdo/compositeArtifacts.xml" //
+        , "HEAD http://127.0.0.1/cdo/child/artifacts.jar" //
+        , "GET  http://127.0.0.1/cdo/child/artifacts.jar" //
+        , "GET  http://127.0.0.1/cdo/child/plugins/org.eclipse.net4j.util_3.3.1.v20140218-1709.jar" //
+        , "     version=1" //
+        , "     metadata.repository.factory.order=compositeContent.xml" //
+        , "     artifact.repository.factory.order=compositeArtifacts.xml" //
+        , "INSTALL 2" //
+        , "HEAD http://127.0.0.1/cdo/compositeContent.jar" //
+        , "HEAD http://127.0.0.1/cdo/compositeContent.xml" //
+        , "HEAD http://127.0.0.1/cdo/content.jar" //
+        , "GET  http://127.0.0.1/cdo/content.jar" //
+        , "HEAD http://127.0.0.1/cdo/compositeArtifacts.jar" //
+        , "HEAD http://127.0.0.1/cdo/compositeArtifacts.xml" //
+        , "HEAD http://127.0.0.1/cdo/artifacts.jar" //
+        , "GET  http://127.0.0.1/cdo/artifacts.jar" //
+        , "GET  http://127.0.0.1/cdo/plugins/org.eclipse.net4j.util_3.3.1.v20140218-1709.jar" //
+        , "     version=1" //
+        , "     metadata.repository.factory.order=content.xml" //
+        , "     artifact.repository.factory.order=artifacts.xml" //
+    );
+  }
+
+  @Test
+  public void test_5_SimpleToCompositeWithoutIndex() throws Exception
+  {
+    List<String> results = test(false, false, false);
+    assertResults(results //
+        , "INSTALL 1" //
+        , "GET  http://127.0.0.1/cdo/p2.index" //
+        , "HEAD http://127.0.0.1/cdo/content.jar" //
+        , "GET  http://127.0.0.1/cdo/content.jar" //
+        , "HEAD http://127.0.0.1/cdo/artifacts.jar" //
+        , "GET  http://127.0.0.1/cdo/artifacts.jar" //
+        , "GET  http://127.0.0.1/cdo/plugins/org.eclipse.net4j.util_3.3.1.v20140218-1709.jar" //
+        , "     version=1" //
+        , "     metadata.repository.factory.order=content.xml" //
+        , "     artifact.repository.factory.order=artifacts.xml" //
+        , "INSTALL 2" //
+        , "HEAD http://127.0.0.1/cdo/content.jar" //
+        , "HEAD http://127.0.0.1/cdo/content.xml" //
+        , "HEAD http://127.0.0.1/cdo/content.xml.xz" //
+        , "HEAD http://127.0.0.1/cdo/compositeContent.jar" //
+        , "HEAD http://127.0.0.1/cdo/compositeContent.xml" //
+        , "GET  http://127.0.0.1/cdo/compositeContent.xml" //
+        , "GET  http://127.0.0.1/cdo/child/p2.index" //
+        , "HEAD http://127.0.0.1/cdo/child/content.jar" //
+        , "GET  http://127.0.0.1/cdo/child/content.jar" //
+        , "HEAD http://127.0.0.1/cdo/artifacts.jar" //
+        , "HEAD http://127.0.0.1/cdo/artifacts.xml" //
+        , "HEAD http://127.0.0.1/cdo/artifacts.xml.xz" //
+        , "HEAD http://127.0.0.1/cdo/compositeArtifacts.jar" //
+        , "HEAD http://127.0.0.1/cdo/compositeArtifacts.xml" //
+        , "GET  http://127.0.0.1/cdo/compositeArtifacts.xml" //
+        , "HEAD http://127.0.0.1/cdo/child/artifacts.jar" //
+        , "GET  http://127.0.0.1/cdo/child/artifacts.jar" //
+        , "GET  http://127.0.0.1/cdo/child/plugins/org.eclipse.net4j.util_3.3.1.v20140218-1709.jar" //
+        , "     version=1" //
+        , "     metadata.repository.factory.order=compositeContent.xml" //
+        , "     artifact.repository.factory.order=compositeArtifacts.xml" //
+    );
+  }
+
+  @Test
+  public void test_6_SimpleToCompositeRemoveIndex() throws Exception
+  {
+    List<String> results = test(false, true, false);
+    assertResults(results //
+        , "INSTALL 1" //
+        , "GET  http://127.0.0.1/cdo/p2.index" //
+        , "HEAD http://127.0.0.1/cdo/content.jar" //
+        , "GET  http://127.0.0.1/cdo/content.jar" //
+        , "HEAD http://127.0.0.1/cdo/artifacts.jar" //
+        , "GET  http://127.0.0.1/cdo/artifacts.jar" //
+        , "GET  http://127.0.0.1/cdo/plugins/org.eclipse.net4j.util_3.3.1.v20140218-1709.jar" //
+        , "     version=1" //
+        , "     metadata.repository.factory.order=content.xml" //
+        , "     artifact.repository.factory.order=artifacts.xml" //
+        , "INSTALL 2" //
+        , "HEAD http://127.0.0.1/cdo/content.jar" //
+        , "HEAD http://127.0.0.1/cdo/content.xml" //
+        , "HEAD http://127.0.0.1/cdo/content.xml.xz" //
+        , "HEAD http://127.0.0.1/cdo/compositeContent.jar" //
+        , "HEAD http://127.0.0.1/cdo/compositeContent.xml" //
+        , "GET  http://127.0.0.1/cdo/compositeContent.xml" //
+        , "GET  http://127.0.0.1/cdo/child/p2.index" //
+        , "HEAD http://127.0.0.1/cdo/child/content.jar" //
+        , "GET  http://127.0.0.1/cdo/child/content.jar" //
+        , "HEAD http://127.0.0.1/cdo/artifacts.jar" //
+        , "HEAD http://127.0.0.1/cdo/artifacts.xml" //
+        , "HEAD http://127.0.0.1/cdo/artifacts.xml.xz" //
+        , "HEAD http://127.0.0.1/cdo/compositeArtifacts.jar" //
+        , "HEAD http://127.0.0.1/cdo/compositeArtifacts.xml" //
+        , "GET  http://127.0.0.1/cdo/compositeArtifacts.xml" //
+        , "HEAD http://127.0.0.1/cdo/child/artifacts.jar" //
+        , "GET  http://127.0.0.1/cdo/child/artifacts.jar" //
+        , "GET  http://127.0.0.1/cdo/child/plugins/org.eclipse.net4j.util_3.3.1.v20140218-1709.jar" //
+        , "     version=1" //
+        , "     metadata.repository.factory.order=compositeContent.xml" //
+        , "     artifact.repository.factory.order=compositeArtifacts.xml" //
+    );
+  }
+
+  @Test
+  public void test_7_SimpleToCompositeAddIndex() throws Exception
+  {
+    List<String> results = test(false, false, true);
+    assertResults(results //
+        , "INSTALL 1" //
+        , "GET  http://127.0.0.1/cdo/p2.index" //
+        , "HEAD http://127.0.0.1/cdo/content.jar" //
+        , "GET  http://127.0.0.1/cdo/content.jar" //
+        , "HEAD http://127.0.0.1/cdo/artifacts.jar" //
+        , "GET  http://127.0.0.1/cdo/artifacts.jar" //
+        , "GET  http://127.0.0.1/cdo/plugins/org.eclipse.net4j.util_3.3.1.v20140218-1709.jar" //
+        , "     version=1" //
+        , "     metadata.repository.factory.order=content.xml" //
+        , "     artifact.repository.factory.order=artifacts.xml" //
+        , "INSTALL 2" //
+        , "HEAD http://127.0.0.1/cdo/content.jar" //
+        , "HEAD http://127.0.0.1/cdo/content.xml" //
+        , "HEAD http://127.0.0.1/cdo/content.xml.xz" //
+        , "HEAD http://127.0.0.1/cdo/compositeContent.jar" //
+        , "HEAD http://127.0.0.1/cdo/compositeContent.xml" //
+        , "GET  http://127.0.0.1/cdo/compositeContent.xml" //
+        , "GET  http://127.0.0.1/cdo/child/p2.index" //
+        , "HEAD http://127.0.0.1/cdo/child/content.jar" //
+        , "GET  http://127.0.0.1/cdo/child/content.jar" //
+        , "HEAD http://127.0.0.1/cdo/artifacts.jar" //
+        , "HEAD http://127.0.0.1/cdo/artifacts.xml" //
+        , "HEAD http://127.0.0.1/cdo/artifacts.xml.xz" //
+        , "HEAD http://127.0.0.1/cdo/compositeArtifacts.jar" //
+        , "HEAD http://127.0.0.1/cdo/compositeArtifacts.xml" //
+        , "GET  http://127.0.0.1/cdo/compositeArtifacts.xml" //
+        , "HEAD http://127.0.0.1/cdo/child/artifacts.jar" //
+        , "GET  http://127.0.0.1/cdo/child/artifacts.jar" //
+        , "GET  http://127.0.0.1/cdo/child/plugins/org.eclipse.net4j.util_3.3.1.v20140218-1709.jar" //
+        , "     version=1" //
+        , "     metadata.repository.factory.order=compositeContent.xml" //
+        , "     artifact.repository.factory.order=compositeArtifacts.xml" //
+    );
+  }
+
+  @Test
+  public void test_8_SimpleToCompositeWithIndex() throws Exception
+  {
+    List<String> results = test(false, true, true);
+    assertResults(results //
+        , "INSTALL 1" //
+        , "GET  http://127.0.0.1/cdo/p2.index" //
+        , "HEAD http://127.0.0.1/cdo/content.jar" //
+        , "GET  http://127.0.0.1/cdo/content.jar" //
+        , "HEAD http://127.0.0.1/cdo/artifacts.jar" //
+        , "GET  http://127.0.0.1/cdo/artifacts.jar" //
+        , "GET  http://127.0.0.1/cdo/plugins/org.eclipse.net4j.util_3.3.1.v20140218-1709.jar" //
+        , "     version=1" //
+        , "     metadata.repository.factory.order=content.xml" //
+        , "     artifact.repository.factory.order=artifacts.xml" //
+        , "INSTALL 2" //
+        , "HEAD http://127.0.0.1/cdo/content.jar" //
+        , "HEAD http://127.0.0.1/cdo/content.xml" //
+        , "HEAD http://127.0.0.1/cdo/content.xml.xz" //
+        , "HEAD http://127.0.0.1/cdo/compositeContent.jar" //
+        , "HEAD http://127.0.0.1/cdo/compositeContent.xml" //
+        , "GET  http://127.0.0.1/cdo/compositeContent.xml" //
+        , "GET  http://127.0.0.1/cdo/child/p2.index" //
+        , "HEAD http://127.0.0.1/cdo/child/content.jar" //
+        , "GET  http://127.0.0.1/cdo/child/content.jar" //
+        , "HEAD http://127.0.0.1/cdo/artifacts.jar" //
+        , "HEAD http://127.0.0.1/cdo/artifacts.xml" //
+        , "HEAD http://127.0.0.1/cdo/artifacts.xml.xz" //
+        , "HEAD http://127.0.0.1/cdo/compositeArtifacts.jar" //
+        , "HEAD http://127.0.0.1/cdo/compositeArtifacts.xml" //
+        , "GET  http://127.0.0.1/cdo/compositeArtifacts.xml" //
+        , "HEAD http://127.0.0.1/cdo/child/artifacts.jar" //
+        , "GET  http://127.0.0.1/cdo/child/artifacts.jar" //
+        , "GET  http://127.0.0.1/cdo/child/plugins/org.eclipse.net4j.util_3.3.1.v20140218-1709.jar" //
+        , "     version=1" //
+        , "     metadata.repository.factory.order=compositeContent.xml" //
+        , "     artifact.repository.factory.order=compositeArtifacts.xml" //
+    );
+  }
+
+  /**
+   * @author Eike Stepper
+   */
+  @SuppressWarnings("restriction")
+  private static final class CountingTransport extends org.eclipse.equinox.internal.p2.repository.Transport
+  {
+    private final org.eclipse.equinox.internal.p2.repository.Transport delegate;
+
+    private final List<String> requests = new ArrayList<String>();
+
+    private CountingTransport(org.eclipse.equinox.internal.p2.repository.Transport delegate)
+    {
+      this.delegate = delegate;
+    }
+
+    public List<String> getRequests()
+    {
+      return requests;
+    }
+
+    @Override
+    public IStatus download(URI uri, OutputStream target, long startPos, IProgressMonitor monitor)
+    {
+      requests.add("GET  " + uri);
+      return delegate.download(uri, target, startPos, monitor);
+    }
+
+    @Override
+    public IStatus download(URI uri, OutputStream target, IProgressMonitor monitor)
+    {
+      requests.add("GET  " + uri);
+      return delegate.download(uri, target, monitor);
+    }
+
+    @Override
+    public InputStream stream(URI uri, IProgressMonitor monitor)
+        throws FileNotFoundException, CoreException, org.eclipse.equinox.internal.p2.repository.AuthenticationFailedException
+    {
+      requests.add("GET  " + uri);
+      return delegate.stream(uri, monitor);
+    }
+
+    @Override
+    public long getLastModified(URI uri, IProgressMonitor monitor)
+        throws CoreException, FileNotFoundException, org.eclipse.equinox.internal.p2.repository.AuthenticationFailedException
+    {
+      requests.add("HEAD " + uri);
+      return delegate.getLastModified(uri, monitor);
+    }
+
+    @Override
+    public boolean equals(Object obj)
+    {
+      return delegate.equals(obj);
+    }
+
+    @Override
+    public int hashCode()
+    {
+      return delegate.hashCode();
+    }
+
+    @Override
+    public String toString()
+    {
+      return requests.toString();
+    }
+  }
+}
diff --git a/plugins/org.eclipse.oomph.p2.ui/src/org/eclipse/oomph/p2/internal/ui/AgentManagerComposite.java b/plugins/org.eclipse.oomph.p2.ui/src/org/eclipse/oomph/p2/internal/ui/AgentManagerComposite.java
index 458d3e3..fe9731f 100644
--- a/plugins/org.eclipse.oomph.p2.ui/src/org/eclipse/oomph/p2/internal/ui/AgentManagerComposite.java
+++ b/plugins/org.eclipse.oomph.p2.ui/src/org/eclipse/oomph/p2/internal/ui/AgentManagerComposite.java
@@ -175,7 +175,7 @@
       @Override
       public void widgetSelected(SelectionEvent e)
       {
-        String path = openDirectoryDialog("Select the location of the new agent.", PropertiesUtil.USER_HOME);
+        String path = openDirectoryDialog("Select the location of the new agent.", PropertiesUtil.getUserHome());
         if (path != null)
         {
           Agent agent = P2Util.getAgentManager().addAgent(new File(path));
diff --git a/plugins/org.eclipse.oomph.setup.core/src/org/eclipse/oomph/setup/internal/core/SetupContext.java b/plugins/org.eclipse.oomph.setup.core/src/org/eclipse/oomph/setup/internal/core/SetupContext.java
index 57df63e..13297db 100644
--- a/plugins/org.eclipse.oomph.setup.core/src/org/eclipse/oomph/setup/internal/core/SetupContext.java
+++ b/plugins/org.eclipse.oomph.setup.core/src/org/eclipse/oomph/setup/internal/core/SetupContext.java
@@ -98,7 +98,7 @@
 
   // State locations
 
-  public static final URI GLOBAL_STATE_LOCATION_URI = URI.createFileURI(PropertiesUtil.USER_HOME).appendSegments(new String[] { ".eclipse", OOMPH_NODE });
+  public static final URI GLOBAL_STATE_LOCATION_URI = URI.createFileURI(PropertiesUtil.getUserHome()).appendSegments(new String[] { ".eclipse", OOMPH_NODE });
 
   public static final URI GLOBAL_SETUPS_URI = URI.createURI(USER_SCHEME + ":/");
 
@@ -106,8 +106,8 @@
 
   public static final URI CONFIGURATION_STATE_LOCATION_URI = CONFIGURATION_LOCATION_URI.appendSegment(OOMPH_NODE);
 
-  public static final URI WORKSPACE_STATE_LOCATION_URI = WORKSPACE_LOCATION_URI == null ? null : WORKSPACE_LOCATION_URI.appendSegments(new String[] {
-      ".metadata", ".plugins", OOMPH_NODE });
+  public static final URI WORKSPACE_STATE_LOCATION_URI = WORKSPACE_LOCATION_URI == null ? null
+      : WORKSPACE_LOCATION_URI.appendSegments(new String[] { ".metadata", ".plugins", OOMPH_NODE });
 
   // Resource locations
 
@@ -125,11 +125,11 @@
 
   public static final URI WORKSPACE_SETUP_FILE_NAME_URI = URI.createURI("workspace.setup");
 
-  public static final URI WORKSPACE_SETUP_URI = WORKSPACE_STATE_LOCATION_URI == null ? null : WORKSPACE_STATE_LOCATION_URI
-      .appendSegment(WORKSPACE_SETUP_FILE_NAME_URI.lastSegment());
+  public static final URI WORKSPACE_SETUP_URI = WORKSPACE_STATE_LOCATION_URI == null ? null
+      : WORKSPACE_STATE_LOCATION_URI.appendSegment(WORKSPACE_SETUP_FILE_NAME_URI.lastSegment());
 
-  public static final URI WORKSPACE_SETUP_RELATIVE_URI = URI.createHierarchicalURI(new String[] { ".metadata", ".plugins", OOMPH_NODE,
-      WORKSPACE_SETUP_FILE_NAME_URI.lastSegment() }, null, null);
+  public static final URI WORKSPACE_SETUP_RELATIVE_URI = URI
+      .createHierarchicalURI(new String[] { ".metadata", ".plugins", OOMPH_NODE, WORKSPACE_SETUP_FILE_NAME_URI.lastSegment() }, null, null);
 
   public static final URI USER_SETUP_URI = GLOBAL_SETUPS_URI.appendSegment("user.setup");
 
@@ -357,8 +357,8 @@
       {
         associate(resourceSet, installation, workspace);
       }
-    }, uriConverter, LOCATION_CATALOG_SETUP_URI, installation == null ? null : installation.eResource().getURI(), workspace == null ? null : workspace
-        .eResource().getURI());
+    }, uriConverter, LOCATION_CATALOG_SETUP_URI, installation == null ? null : installation.eResource().getURI(),
+        workspace == null ? null : workspace.eResource().getURI());
   }
 
   private static void associate(ResourceSet resourceSet, Installation installation, Workspace workspace)
diff --git a/plugins/org.eclipse.oomph.setup.installer/src/org/eclipse/oomph/setup/internal/installer/KeepInstallerDialog.java b/plugins/org.eclipse.oomph.setup.installer/src/org/eclipse/oomph/setup/internal/installer/KeepInstallerDialog.java
index d79621d..1cb2b62 100644
--- a/plugins/org.eclipse.oomph.setup.installer/src/org/eclipse/oomph/setup/internal/installer/KeepInstallerDialog.java
+++ b/plugins/org.eclipse.oomph.setup.installer/src/org/eclipse/oomph/setup/internal/installer/KeepInstallerDialog.java
@@ -175,7 +175,7 @@
     {
       public void run()
       {
-        File home = new File(PropertiesUtil.USER_HOME);
+        File home = new File(PropertiesUtil.getUserHome());
         for (int i = 1; i < Integer.MAX_VALUE; i++)
         {
           File folder = new File(home, "eclipse-installer" + (i > 1 ? i : ""));
diff --git a/plugins/org.eclipse.oomph.setup.installer/src/org/eclipse/oomph/setup/internal/installer/KeepInstallerUtil.java b/plugins/org.eclipse.oomph.setup.installer/src/org/eclipse/oomph/setup/internal/installer/KeepInstallerUtil.java
index 0270c15..da03f07 100644
--- a/plugins/org.eclipse.oomph.setup.installer/src/org/eclipse/oomph/setup/internal/installer/KeepInstallerUtil.java
+++ b/plugins/org.eclipse.oomph.setup.installer/src/org/eclipse/oomph/setup/internal/installer/KeepInstallerUtil.java
@@ -106,7 +106,7 @@
     if (!isInstallerKept() && OS.INSTANCE.isWin())
     {
       String launcher = InstallerApplication.getLauncher();
-      return launcher != null && launcher.startsWith(PropertiesUtil.TEMP_DIR);
+      return launcher != null && launcher.startsWith(PropertiesUtil.getTmpDir());
     }
 
     return false;
diff --git a/plugins/org.eclipse.oomph.setup.installer/src/org/eclipse/oomph/setup/internal/installer/SimpleKeepInstallerPage.java b/plugins/org.eclipse.oomph.setup.installer/src/org/eclipse/oomph/setup/internal/installer/SimpleKeepInstallerPage.java
index e064617..4913a8f 100644
--- a/plugins/org.eclipse.oomph.setup.installer/src/org/eclipse/oomph/setup/internal/installer/SimpleKeepInstallerPage.java
+++ b/plugins/org.eclipse.oomph.setup.installer/src/org/eclipse/oomph/setup/internal/installer/SimpleKeepInstallerPage.java
@@ -239,7 +239,7 @@
     {
       public void run()
       {
-        File home = new File(PropertiesUtil.USER_HOME);
+        File home = new File(PropertiesUtil.getUserHome());
         for (int i = 1; i < Integer.MAX_VALUE; i++)
         {
           File folder = new File(home, "eclipse-installer" + (i > 1 ? i : ""));
diff --git a/plugins/org.eclipse.oomph.setup.installer/src/org/eclipse/oomph/setup/internal/installer/SimpleVariablePage.java b/plugins/org.eclipse.oomph.setup.installer/src/org/eclipse/oomph/setup/internal/installer/SimpleVariablePage.java
index 662664c..3e69a65 100644
--- a/plugins/org.eclipse.oomph.setup.installer/src/org/eclipse/oomph/setup/internal/installer/SimpleVariablePage.java
+++ b/plugins/org.eclipse.oomph.setup.installer/src/org/eclipse/oomph/setup/internal/installer/SimpleVariablePage.java
@@ -907,7 +907,7 @@
       {
         // Default to ${user.home}/eclipse, unless there is a file at that location, or what looks like an existing Eclipse installation.
         // In that case default just to ${user.home}.
-        String defaultValue = PropertiesUtil.USER_HOME;
+        String defaultValue = PropertiesUtil.getUserHome();
         File defaultInstallRoot = new File(defaultValue, "eclipse");
         if (!defaultInstallRoot.exists())
         {
diff --git a/plugins/org.eclipse.oomph.targlets.tests/META-INF/MANIFEST.MF b/plugins/org.eclipse.oomph.targlets.tests/META-INF/MANIFEST.MF
index 13074e6..19fb9b8 100644
--- a/plugins/org.eclipse.oomph.targlets.tests/META-INF/MANIFEST.MF
+++ b/plugins/org.eclipse.oomph.targlets.tests/META-INF/MANIFEST.MF
@@ -2,12 +2,12 @@
 Bundle-ManifestVersion: 2
 Bundle-Name: %pluginName
 Bundle-SymbolicName: org.eclipse.oomph.targlets.tests;singleton:=true
-Bundle-Version: 1.1.0.qualifier
+Bundle-Version: 1.2.0.qualifier
 Bundle-ClassPath: .
 Bundle-Vendor: %providerName
 Bundle-Localization: plugin
 Bundle-RequiredExecutionEnvironment: J2SE-1.5
-Export-Package: org.eclipse.oomph.targlets.tests;version="1.1.0";x-internal:=true
+Export-Package: org.eclipse.oomph.targlets.tests;version="1.2.0";x-internal:=true
 Require-Bundle: org.eclipse.oomph.p2.tests;bundle-version="[1.1.0,2.0.0)";visibility:=reexport,
  org.eclipse.oomph.targlets.core;bundle-version="[1.1.0,2.0.0)";visibility:=reexport,
  org.eclipse.oomph.util.pde;bundle-version="[1.1.0,2.0.0)",
diff --git a/plugins/org.eclipse.oomph.targlets.tests/TargletTests.launch b/plugins/org.eclipse.oomph.targlets.tests/TargletTests.launch
index 4046a60..4ba16b1 100644
--- a/plugins/org.eclipse.oomph.targlets.tests/TargletTests.launch
+++ b/plugins/org.eclipse.oomph.targlets.tests/TargletTests.launch
@@ -1,7 +1,6 @@
 <?xml version="1.0" encoding="UTF-8" standalone="no"?>
 <launchConfiguration type="org.eclipse.pde.ui.JunitLaunchConfig">
 <booleanAttribute key="append.args" value="true"/>
-<stringAttribute key="application" value="org.eclipse.pde.junit.runtime.coretestapplication"/>
 <booleanAttribute key="askclear" value="false"/>
 <booleanAttribute key="automaticAdd" value="false"/>
 <booleanAttribute key="automaticValidate" value="true"/>
@@ -33,7 +32,7 @@
 <stringAttribute key="pde.version" value="3.3"/>
 <stringAttribute key="product" value="org.eclipse.equinox.p2.director.app.product"/>
 <booleanAttribute key="run_in_ui_thread" value="true"/>
-<stringAttribute key="selected_target_plugins" value="com.ibm.icu@default:default,javax.annotation@default:default,javax.inject@default:default,javax.xml@default:default,org.apache.batik.css@default:default,org.apache.batik.util.gui@default:default,org.apache.batik.util@default:default,org.apache.commons.codec@default:default,org.apache.commons.logging@default:default,org.apache.felix.gogo.command@default:default,org.apache.felix.gogo.runtime@default:default,org.apache.felix.gogo.shell@default:default,org.apache.httpcomponents.httpclient@default:default,org.apache.httpcomponents.httpcore@default:default,org.eclipse.ant.core@default:default,org.eclipse.compare.core@default:default,org.eclipse.core.commands@default:default,org.eclipse.core.contenttype@default:default,org.eclipse.core.databinding.observable@default:default,org.eclipse.core.databinding.property@default:default,org.eclipse.core.databinding@default:default,org.eclipse.core.expressions@default:default,org.eclipse.core.filebuffers@default:default,org.eclipse.core.filesystem.java7@default:false,org.eclipse.core.filesystem.win32.x86_64@default:false,org.eclipse.core.filesystem@default:default,org.eclipse.core.jobs@default:default,org.eclipse.core.net.win32.x86_64@default:false,org.eclipse.core.net@default:default,org.eclipse.core.resources.win32.x86_64@default:false,org.eclipse.core.resources@default:default,org.eclipse.core.runtime.compatibility.registry@default:false,org.eclipse.core.runtime@default:true,org.eclipse.core.variables@default:default,org.eclipse.debug.core@default:default,org.eclipse.e4.core.commands@default:default,org.eclipse.e4.core.contexts@default:default,org.eclipse.e4.core.di.extensions@default:default,org.eclipse.e4.core.di@default:default,org.eclipse.e4.core.services@default:default,org.eclipse.ecf.filetransfer@default:default,org.eclipse.ecf.identity@default:default,org.eclipse.ecf.provider.filetransfer.httpclient4.ssl@default:false,org.eclipse.ecf.provider.filetransfer.httpclient4@default:default,org.eclipse.ecf.provider.filetransfer.ssl@default:false,org.eclipse.ecf.provider.filetransfer@default:default,org.eclipse.ecf.ssl@default:false,org.eclipse.ecf@default:default,org.eclipse.emf.common@default:default,org.eclipse.emf.ecore.change@default:default,org.eclipse.emf.ecore.xmi@default:default,org.eclipse.emf.ecore@default:default,org.eclipse.equinox.app@default:default,org.eclipse.equinox.common@2:true,org.eclipse.equinox.concurrent@default:default,org.eclipse.equinox.console@default:default,org.eclipse.equinox.ds@1:true,org.eclipse.equinox.frameworkadmin.equinox@default:default,org.eclipse.equinox.frameworkadmin@default:default,org.eclipse.equinox.p2.artifact.repository@default:default,org.eclipse.equinox.p2.console@default:default,org.eclipse.equinox.p2.core@default:default,org.eclipse.equinox.p2.director.app@default:default,org.eclipse.equinox.p2.director@default:default,org.eclipse.equinox.p2.engine@default:default,org.eclipse.equinox.p2.garbagecollector@default:default,org.eclipse.equinox.p2.jarprocessor@default:default,org.eclipse.equinox.p2.metadata.repository@default:default,org.eclipse.equinox.p2.metadata@default:default,org.eclipse.equinox.p2.operations@default:default,org.eclipse.equinox.p2.publisher.eclipse@default:default,org.eclipse.equinox.p2.publisher@default:default,org.eclipse.equinox.p2.repository.tools@default:default,org.eclipse.equinox.p2.repository@default:default,org.eclipse.equinox.p2.touchpoint.eclipse@default:default,org.eclipse.equinox.p2.touchpoint.natives@default:default,org.eclipse.equinox.p2.transport.ecf@default:default,org.eclipse.equinox.p2.updatesite@default:default,org.eclipse.equinox.preferences@default:default,org.eclipse.equinox.registry@default:default,org.eclipse.equinox.security.win32.x86_64@default:false,org.eclipse.equinox.security@default:default,org.eclipse.equinox.simpleconfigurator.manipulator@default:default,org.eclipse.equinox.simpleconfigurator@1:true,org.eclipse.equinox.util@default:default,org.eclipse.help@default:default,org.eclipse.jdt.compiler.apt@default:default,org.eclipse.jdt.compiler.tool@default:default,org.eclipse.jdt.core@default:default,org.eclipse.jdt.debug@default:default,org.eclipse.jdt.launching@default:default,org.eclipse.jface.databinding@default:default,org.eclipse.jface@default:default,org.eclipse.osgi.compatibility.state@default:false,org.eclipse.osgi.services@default:default,org.eclipse.osgi@-1:true,org.eclipse.pde.build@default:default,org.eclipse.pde.core@default:default,org.eclipse.swt.win32.win32.x86_64@default:false,org.eclipse.swt@default:default,org.eclipse.team.core@default:default,org.eclipse.text@default:default,org.eclipse.update.configurator@3:true,org.hamcrest.core@default:default,org.junit@default:default,org.sat4j.core@default:default,org.sat4j.pb@default:default,org.w3c.css.sac@default:default,org.w3c.dom.events@default:default,org.w3c.dom.smil@default:default,org.w3c.dom.svg@default:default"/>
+<stringAttribute key="selected_target_plugins" value="com.ibm.icu@default:default,javax.annotation@default:default,javax.inject@default:default,javax.xml@default:default,org.apache.batik.css@default:default,org.apache.batik.util.gui@default:default,org.apache.batik.util@default:default,org.apache.commons.codec@default:default,org.apache.commons.logging@default:default,org.apache.felix.gogo.command@default:default,org.apache.felix.gogo.runtime@default:default,org.apache.felix.gogo.shell@default:default,org.apache.httpcomponents.httpclient@default:default,org.apache.httpcomponents.httpcore@default:default,org.eclipse.ant.core@default:default,org.eclipse.compare.core@default:default,org.eclipse.core.commands@default:default,org.eclipse.core.contenttype@default:default,org.eclipse.core.databinding.observable@default:default,org.eclipse.core.databinding.property@default:default,org.eclipse.core.databinding@default:default,org.eclipse.core.expressions@default:default,org.eclipse.core.filebuffers@default:default,org.eclipse.core.filesystem.java7@default:false,org.eclipse.core.filesystem.win32.x86_64@default:false,org.eclipse.core.filesystem@default:default,org.eclipse.core.jobs@default:default,org.eclipse.core.net.win32.x86_64@default:false,org.eclipse.core.net@default:default,org.eclipse.core.resources.win32.x86_64@default:false,org.eclipse.core.resources@default:default,org.eclipse.core.runtime.compatibility.registry@default:false,org.eclipse.core.runtime@default:true,org.eclipse.core.variables@default:default,org.eclipse.debug.core@default:default,org.eclipse.e4.core.commands@default:default,org.eclipse.e4.core.contexts@default:default,org.eclipse.e4.core.di.extensions@default:default,org.eclipse.e4.core.di@default:default,org.eclipse.e4.core.services@default:default,org.eclipse.ecf.filetransfer@default:default,org.eclipse.ecf.identity@default:default,org.eclipse.ecf.provider.filetransfer.httpclient4.ssl@default:false,org.eclipse.ecf.provider.filetransfer.httpclient4@default:default,org.eclipse.ecf.provider.filetransfer.ssl@default:false,org.eclipse.ecf.provider.filetransfer@default:default,org.eclipse.ecf.ssl@default:false,org.eclipse.ecf@default:default,org.eclipse.emf.common@default:default,org.eclipse.emf.ecore.change@default:default,org.eclipse.emf.ecore.xmi@default:default,org.eclipse.emf.ecore@default:default,org.eclipse.equinox.app@default:default,org.eclipse.equinox.common@2:true,org.eclipse.equinox.concurrent@default:default,org.eclipse.equinox.console@default:default,org.eclipse.equinox.ds@1:true,org.eclipse.equinox.frameworkadmin.equinox@default:default,org.eclipse.equinox.frameworkadmin@default:default,org.eclipse.equinox.p2.artifact.repository@default:default,org.eclipse.equinox.p2.console@default:default,org.eclipse.equinox.p2.core@default:default,org.eclipse.equinox.p2.director.app@default:default,org.eclipse.equinox.p2.director@default:default,org.eclipse.equinox.p2.engine@default:default,org.eclipse.equinox.p2.garbagecollector@default:default,org.eclipse.equinox.p2.jarprocessor@default:default,org.eclipse.equinox.p2.metadata.repository@default:default,org.eclipse.equinox.p2.metadata@default:default,org.eclipse.equinox.p2.operations@default:default,org.eclipse.equinox.p2.publisher.eclipse@default:default,org.eclipse.equinox.p2.publisher@default:default,org.eclipse.equinox.p2.repository.tools@default:default,org.eclipse.equinox.p2.repository@default:default,org.eclipse.equinox.p2.touchpoint.eclipse@default:default,org.eclipse.equinox.p2.touchpoint.natives@default:default,org.eclipse.equinox.p2.transport.ecf@default:default,org.eclipse.equinox.p2.updatesite@default:default,org.eclipse.equinox.preferences@default:default,org.eclipse.equinox.registry@default:default,org.eclipse.equinox.security.win32.x86_64@default:false,org.eclipse.equinox.security@default:default,org.eclipse.equinox.simpleconfigurator.manipulator@default:default,org.eclipse.equinox.simpleconfigurator@1:true,org.eclipse.equinox.util@default:default,org.eclipse.help@default:default,org.eclipse.jdt.compiler.apt@default:false,org.eclipse.jdt.compiler.tool@default:false,org.eclipse.jdt.core@default:default,org.eclipse.jdt.debug@default:default,org.eclipse.jdt.launching@default:default,org.eclipse.jface.databinding@default:default,org.eclipse.jface@default:default,org.eclipse.osgi.compatibility.state@default:false,org.eclipse.osgi.services@default:default,org.eclipse.osgi@-1:true,org.eclipse.pde.build@default:default,org.eclipse.pde.core@default:default,org.eclipse.swt.win32.win32.x86_64@default:false,org.eclipse.swt@default:default,org.eclipse.team.core@default:default,org.eclipse.text@default:default,org.eclipse.update.configurator@3:true,org.hamcrest.core@default:default,org.junit@default:default,org.sat4j.core@default:default,org.sat4j.pb@default:default,org.w3c.css.sac@default:default,org.w3c.dom.events@default:default,org.w3c.dom.smil@default:default,org.w3c.dom.svg@default:default"/>
 <stringAttribute key="selected_workspace_plugins" value="org.eclipse.oomph.base@default:default,org.eclipse.oomph.p2.core@default:default,org.eclipse.oomph.p2.tests@default:default,org.eclipse.oomph.p2@default:default,org.eclipse.oomph.predicates@default:default,org.eclipse.oomph.resources@default:default,org.eclipse.oomph.targlets.core@default:default,org.eclipse.oomph.targlets.tests@default:default,org.eclipse.oomph.targlets@default:default,org.eclipse.oomph.tests@default:default,org.eclipse.oomph.util@default:default"/>
 <booleanAttribute key="show_selected_only" value="false"/>
 <stringAttribute key="templateConfig" value="${target_home}\configuration\config.ini"/>
diff --git a/plugins/org.eclipse.oomph.targlets.tests/pom.xml b/plugins/org.eclipse.oomph.targlets.tests/pom.xml
index 47ddc17..b3a27dd 100644
--- a/plugins/org.eclipse.oomph.targlets.tests/pom.xml
+++ b/plugins/org.eclipse.oomph.targlets.tests/pom.xml
@@ -20,7 +20,7 @@
   </parent>
   <groupId>org.eclipse.oomph</groupId>
   <artifactId>org.eclipse.oomph.targlets.tests</artifactId>
-  <version>1.1.0-SNAPSHOT</version>
+  <version>1.2.0-SNAPSHOT</version>
   <packaging>eclipse-plugin</packaging>
 
   <build>
diff --git a/plugins/org.eclipse.oomph.targlets.tests/src/org/eclipse/oomph/targlets/tests/TargletTests.java b/plugins/org.eclipse.oomph.targlets.tests/src/org/eclipse/oomph/targlets/tests/TargletTests.java
index 6a2179a..dc9a3e3 100644
--- a/plugins/org.eclipse.oomph.targlets.tests/src/org/eclipse/oomph/targlets/tests/TargletTests.java
+++ b/plugins/org.eclipse.oomph.targlets.tests/src/org/eclipse/oomph/targlets/tests/TargletTests.java
@@ -42,7 +42,9 @@
 import org.eclipse.pde.core.target.TargetBundle;
 import org.eclipse.pde.core.target.TargetFeature;
 
+import org.junit.FixMethodOrder;
 import org.junit.Test;
+import org.junit.runners.MethodSorters;
 
 import java.io.File;
 import java.util.Arrays;
@@ -53,6 +55,7 @@
 /**
  * @author Eike Stepper
  */
+@FixMethodOrder(MethodSorters.JVM)
 public class TargletTests extends AbstractP2Test
 {
   private static final String TARGET_NAME = "Test Target";
diff --git a/plugins/org.eclipse.oomph.tests/META-INF/MANIFEST.MF b/plugins/org.eclipse.oomph.tests/META-INF/MANIFEST.MF
index 9c5680c..84458c3 100644
--- a/plugins/org.eclipse.oomph.tests/META-INF/MANIFEST.MF
+++ b/plugins/org.eclipse.oomph.tests/META-INF/MANIFEST.MF
@@ -2,12 +2,12 @@
 Bundle-ManifestVersion: 2
 Bundle-Name: %pluginName
 Bundle-SymbolicName: org.eclipse.oomph.tests;singleton:=true
-Bundle-Version: 1.1.0.qualifier
+Bundle-Version: 1.2.0.qualifier
 Bundle-ClassPath: .
 Bundle-Vendor: %providerName
 Bundle-Localization: plugin
 Bundle-RequiredExecutionEnvironment: J2SE-1.5
-Export-Package: org.eclipse.oomph.tests;version="1.1.0";x-internal:=true
+Export-Package: org.eclipse.oomph.tests;version="1.2.0";x-internal:=true
 Require-Bundle: org.eclipse.core.runtime;bundle-version="[3.5.0,4.0.0)";visibility:=reexport,
  org.eclipse.core.net;bundle-version="[1.0.0,2.0.0)";visibility:=reexport,
  org.eclipse.oomph.util;bundle-version="[1.1.0,2.0.0)";visibility:=reexport,
diff --git a/plugins/org.eclipse.oomph.tests/pom.xml b/plugins/org.eclipse.oomph.tests/pom.xml
index df6e53a..077da1c 100644
--- a/plugins/org.eclipse.oomph.tests/pom.xml
+++ b/plugins/org.eclipse.oomph.tests/pom.xml
@@ -20,7 +20,7 @@
   </parent>
   <groupId>org.eclipse.oomph</groupId>
   <artifactId>org.eclipse.oomph.tests</artifactId>
-  <version>1.1.0-SNAPSHOT</version>
+  <version>1.2.0-SNAPSHOT</version>
   <packaging>eclipse-plugin</packaging>
 
   <build>
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 8cd28d2..9460579 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
@@ -127,6 +127,7 @@
     if (userHome == null)
     {
       userHome = createTempFolder();
+      System.setProperty("user.home", userHome.getAbsolutePath());
     }
 
     return userHome;
diff --git a/plugins/org.eclipse.oomph.util/META-INF/MANIFEST.MF b/plugins/org.eclipse.oomph.util/META-INF/MANIFEST.MF
index 40cd6fe..0b36d19 100644
--- a/plugins/org.eclipse.oomph.util/META-INF/MANIFEST.MF
+++ b/plugins/org.eclipse.oomph.util/META-INF/MANIFEST.MF
@@ -1,7 +1,7 @@
 Manifest-Version: 1.0
 Bundle-ManifestVersion: 2
 Bundle-SymbolicName: org.eclipse.oomph.util;singleton:=true
-Bundle-Version: 1.1.0.qualifier
+Bundle-Version: 1.2.0.qualifier
 Bundle-Name: %pluginName
 Bundle-Vendor: %providerName
 Bundle-Localization: plugin
@@ -10,9 +10,10 @@
 Bundle-RequiredExecutionEnvironment: J2SE-1.5
 Bundle-ClassPath: .
 Require-Bundle: org.eclipse.core.runtime;bundle-version="[3.5.0,4.0.0)",
+ org.apache.httpcomponents.httpclient;bundle-version="[4.0.0,5.0.0)",
  org.eclipse.emf.common;bundle-version="[2.10.0,3.0.0)";visibility:=reexport
 Import-Package: org.osgi.framework;version="[1.3.0,2.0.0)"
-Export-Package: org.eclipse.oomph.internal.util;version="1.1.0";x-internal:=true,
- org.eclipse.oomph.internal.util.table;version="1.1.0";x-internal:=true,
- org.eclipse.oomph.util;version="1.1.0";x-internal:=true
+Export-Package: org.eclipse.oomph.internal.util;version="1.2.0";x-internal:=true,
+ org.eclipse.oomph.internal.util.table;version="1.2.0";x-internal:=true,
+ org.eclipse.oomph.util;version="1.2.0";x-internal:=true
 Eclipse-BuddyPolicy: registered
diff --git a/plugins/org.eclipse.oomph.util/pom.xml b/plugins/org.eclipse.oomph.util/pom.xml
index 71d17a8..ba43894 100644
--- a/plugins/org.eclipse.oomph.util/pom.xml
+++ b/plugins/org.eclipse.oomph.util/pom.xml
@@ -20,7 +20,7 @@
   </parent>
   <groupId>org.eclipse.oomph</groupId>
   <artifactId>org.eclipse.oomph.util</artifactId>
-  <version>1.1.0-SNAPSHOT</version>
+  <version>1.2.0-SNAPSHOT</version>
   <packaging>eclipse-plugin</packaging>
 
   <build>
diff --git a/plugins/org.eclipse.oomph.util/src/org/eclipse/oomph/internal/util/HTTPServer.java b/plugins/org.eclipse.oomph.util/src/org/eclipse/oomph/internal/util/HTTPServer.java
index 135f587..cd9830d 100644
--- a/plugins/org.eclipse.oomph.util/src/org/eclipse/oomph/internal/util/HTTPServer.java
+++ b/plugins/org.eclipse.oomph.util/src/org/eclipse/oomph/internal/util/HTTPServer.java
@@ -41,6 +41,7 @@
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.Comparator;
+import java.util.Date;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -126,6 +127,12 @@
     return 0;
   }
 
+  public String getURL()
+  {
+    int port = acceptor != null ? acceptor.getPort() : 0;
+    return "http://127.0.0.1:" + port;
+  }
+
   public synchronized void addContext(Context context)
   {
     contexts.add(context);
@@ -159,6 +166,12 @@
     }
   }
 
+  @Override
+  public String toString()
+  {
+    return getURL();
+  }
+
   private static void registerContentType(String contentType, String... extensions)
   {
     for (String extension : extensions)
@@ -329,7 +342,7 @@
               UtilPlugin.INSTANCE.log(ex);
             }
 
-            Context.sendResponse(output, STATUS_INTERNAL_SERVER_ERROR, null, true);
+            Context.sendResponse(output, STATUS_INTERNAL_SERVER_ERROR, null, 0, true);
           }
 
           try
@@ -376,14 +389,16 @@
       String[] tokens = line.split(" ");
       if (tokens.length < 2)
       {
-        Context.sendResponse(output, STATUS_BAD_REQUEST, null, false);
+        Context.sendResponse(output, STATUS_BAD_REQUEST, null, 0, false);
         return;
       }
 
       String method = tokens[0];
-      if (!"GET".equalsIgnoreCase(method))
+      boolean head = "HEAD".equalsIgnoreCase(method);
+
+      if (!head && !"GET".equalsIgnoreCase(method))
       {
-        Context.sendResponse(output, STATUS_NOT_IMPLEMENTED, null, false);
+        Context.sendResponse(output, STATUS_NOT_IMPLEMENTED, null, 0, false);
         return;
       }
 
@@ -395,7 +410,7 @@
       {
         if (PATH_SEPARATOR.equals(path))
         {
-          Context.sendResponse(output, STATUS_OK, "index.html", false);
+          Context.sendResponse(output, STATUS_OK, "index.html", 0, false);
 
           for (Context c : contexts)
           {
@@ -410,7 +425,7 @@
           return;
         }
 
-        Context.sendResponse(output, STATUS_NOT_FOUND, null, false);
+        Context.sendResponse(output, STATUS_NOT_FOUND, null, 0, false);
         return;
       }
 
@@ -425,7 +440,7 @@
         System.out.println(context + " " + path);
       }
 
-      context.handleRequest(path, output);
+      context.handleRequest(path, output, !head);
     }
 
     private boolean isBadState()
@@ -447,6 +462,11 @@
 
     protected Context(String path, boolean allowDirectory)
     {
+      if (!path.startsWith(PATH_SEPARATOR))
+      {
+        throw new IllegalArgumentException("Path must start with a slash: " + path);
+      }
+
       this.path = path;
       this.allowDirectory = allowDirectory;
     }
@@ -471,6 +491,11 @@
       return null;
     }
 
+    public String getURL(HTTPServer server)
+    {
+      return server.getURL() + path;
+    }
+
     @Override
     public final String toString()
     {
@@ -484,24 +509,24 @@
       return string + "]";
     }
 
-    protected void handleRequest(String path, DataOutputStream output) throws IOException
+    protected void handleRequest(String path, DataOutputStream output, boolean responseBody) throws IOException
     {
       if (isDirectory(path))
       {
         if (!allowDirectory)
         {
-          Context.sendResponse(output, STATUS_FORBIDDEN, null, false);
+          Context.sendResponse(output, STATUS_FORBIDDEN, null, 0, false);
         }
         else
         {
           if (!path.endsWith(PATH_SEPARATOR))
           {
             path += PATH_SEPARATOR;
-            Context.sendResponse(output, STATUS_SEE_OTHER, path, false);
+            Context.sendResponse(output, STATUS_SEE_OTHER, path, 0, false);
           }
           else
           {
-            Context.sendResponse(output, STATUS_OK, "index.html", false);
+            Context.sendResponse(output, STATUS_OK, "index.html", 0, false);
           }
 
           if (path.length() > 1)
@@ -553,21 +578,26 @@
 
       if (!isFile(path))
       {
-        Context.sendResponse(output, STATUS_NOT_FOUND, null, false);
+        Context.sendResponse(output, STATUS_NOT_FOUND, null, 0, false);
         return;
       }
 
-      Context.sendResponse(output, STATUS_OK, path, false);
-      InputStream stream = null;
+      long lastModified = getLastModified(path);
+      Context.sendResponse(output, STATUS_OK, path, lastModified, false);
 
-      try
+      if (responseBody)
       {
-        stream = getContents(path);
-        IOUtil.copy(stream, output);
-      }
-      finally
-      {
-        IOUtil.close(stream);
+        InputStream stream = null;
+
+        try
+        {
+          stream = getContents(path);
+          IOUtil.copy(stream, output);
+        }
+        finally
+        {
+          IOUtil.close(stream);
+        }
       }
     }
 
@@ -579,6 +609,11 @@
 
     protected abstract InputStream getContents(String path) throws IOException;
 
+    protected long getLastModified(String path) throws IOException
+    {
+      return System.currentTimeMillis();
+    }
+
     protected static String encodePath(String path) throws UnsupportedEncodingException
     {
       StringBuilder builder = new StringBuilder();
@@ -602,7 +637,13 @@
       return URLDecoder.decode(path, URL_ENCODING);
     }
 
-    protected static void sendResponse(DataOutputStream output, String status, String fileName, boolean ignoreExceptions)
+    @SuppressWarnings({ "deprecation", "restriction" })
+    protected static String formatDate(long lastModified)
+    {
+      return org.apache.http.impl.cookie.DateUtils.formatDate(new Date(lastModified));
+    }
+
+    protected static void sendResponse(DataOutputStream output, String status, String fileName, long lastModified, boolean ignoreExceptions)
     {
       try
       {
@@ -632,6 +673,13 @@
         output.writeBytes(contentType);
         output.writeBytes("\r\n");
 
+        if (lastModified != 0)
+        {
+          output.writeBytes("Last-Modified: ");
+          output.writeBytes(formatDate(lastModified));
+          output.writeBytes("\r\n");
+        }
+
         if (location != null)
         {
           output.writeBytes("Location: /file/c");
@@ -653,10 +701,17 @@
       }
       catch (IOException ex)
       {
-        if (!ignoreExceptions)
+        if (ignoreExceptions)
         {
-          UtilPlugin.INSTANCE.log(ex);
+          return;
         }
+
+        if (ex instanceof SocketException && ex.getMessage().equals("Software caused connection abort: socket write error"))
+        {
+          return;
+        }
+
+        UtilPlugin.INSTANCE.log(ex);
       }
     }
   }
@@ -822,6 +877,13 @@
       return new FileInputStream(file);
     }
 
+    @Override
+    protected long getLastModified(String path) throws IOException
+    {
+      File file = getFile(path);
+      return file.lastModified();
+    }
+
     private File getFile(String path)
     {
       if (root == null)
@@ -927,6 +989,13 @@
       return file.getContents();
     }
 
+    @Override
+    protected long getLastModified(String path) throws IOException
+    {
+      BundleFile file = getFile(path);
+      return file.getBundle().getLastModified();
+    }
+
     private BundleFile getFile(String path) throws FileNotFoundException
     {
       String[] segments = path.split(PATH_SEPARATOR);
diff --git a/plugins/org.eclipse.oomph.util/src/org/eclipse/oomph/util/OomphPlugin.java b/plugins/org.eclipse.oomph.util/src/org/eclipse/oomph/util/OomphPlugin.java
index 1f5de41..7e2d5bd 100644
--- a/plugins/org.eclipse.oomph.util/src/org/eclipse/oomph/util/OomphPlugin.java
+++ b/plugins/org.eclipse.oomph.util/src/org/eclipse/oomph/util/OomphPlugin.java
@@ -97,7 +97,7 @@
 
   public final IPath getUserLocation() throws IllegalStateException
   {
-    return new Path(PropertiesUtil.USER_HOME).append(".eclipse").append(getSymbolicName());
+    return new Path(PropertiesUtil.getUserHome()).append(".eclipse").append(getSymbolicName());
   }
 
   public final Preferences getInstancePreferences()
diff --git a/plugins/org.eclipse.oomph.util/src/org/eclipse/oomph/util/PropertiesUtil.java b/plugins/org.eclipse.oomph.util/src/org/eclipse/oomph/util/PropertiesUtil.java
index 349c48f..480a873 100644
--- a/plugins/org.eclipse.oomph.util/src/org/eclipse/oomph/util/PropertiesUtil.java
+++ b/plugins/org.eclipse.oomph.util/src/org/eclipse/oomph/util/PropertiesUtil.java
@@ -33,10 +33,6 @@
  */
 public final class PropertiesUtil
 {
-  public static final String USER_HOME = getProperty("user.home", ".");
-
-  public static final String TEMP_DIR = getProperty("java.io.tmpdir", ".");
-
   public static final String[] EXPERT_FILTER = { "org.eclipse.ui.views.properties.expert" };
 
   private static final String TRUE = Boolean.TRUE.toString();
@@ -106,6 +102,16 @@
     }
   }
 
+  public static String getUserHome()
+  {
+    return getProperty("user.home", ".");
+  }
+
+  public static String getTmpDir()
+  {
+    return getProperty("java.io.tmpdir", ".");
+  }
+
   // public static void main(String[] args)
   // {
   // Map<String, String> properties = loadProperties(new File("config.ini"));