You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ace.apache.org by ma...@apache.org on 2009/06/27 17:53:26 UTC

svn commit: r788992 [24/25] - in /incubator/ace/trunk: gateway/ gateway/src/ gateway/src/net/ gateway/src/net/luminis/ gateway/src/net/luminis/liq/ gateway/src/net/luminis/liq/bootstrap/ gateway/src/net/luminis/liq/bootstrap/multigateway/ gateway/src/n...

Added: incubator/ace/trunk/test/src/net/luminis/liq/test/repositoryadmin/RepositoryAdminTest.java
URL: http://svn.apache.org/viewvc/incubator/ace/trunk/test/src/net/luminis/liq/test/repositoryadmin/RepositoryAdminTest.java?rev=788992&view=auto
==============================================================================
--- incubator/ace/trunk/test/src/net/luminis/liq/test/repositoryadmin/RepositoryAdminTest.java (added)
+++ incubator/ace/trunk/test/src/net/luminis/liq/test/repositoryadmin/RepositoryAdminTest.java Sat Jun 27 15:53:04 2009
@@ -0,0 +1,1928 @@
+package net.luminis.liq.test.repositoryadmin;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+import java.io.Reader;
+import java.io.Writer;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Dictionary;
+import java.util.HashMap;
+import java.util.Hashtable;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.concurrent.Callable;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
+import java.util.jar.Attributes;
+import java.util.jar.JarOutputStream;
+import java.util.jar.Manifest;
+
+import net.luminis.liq.client.repository.ObjectRepository;
+import net.luminis.liq.client.repository.RepositoryAdmin;
+import net.luminis.liq.client.repository.RepositoryAdminLoginContext;
+import net.luminis.liq.client.repository.RepositoryObject;
+import net.luminis.liq.client.repository.RepositoryObject.WorkingState;
+import net.luminis.liq.client.repository.helper.ArtifactHelper;
+import net.luminis.liq.client.repository.helper.ArtifactPreprocessor;
+import net.luminis.liq.client.repository.helper.PropertyResolver;
+import net.luminis.liq.client.repository.helper.bundle.BundleHelper;
+import net.luminis.liq.client.repository.object.Artifact2GroupAssociation;
+import net.luminis.liq.client.repository.object.ArtifactObject;
+import net.luminis.liq.client.repository.object.DeploymentArtifact;
+import net.luminis.liq.client.repository.object.DeploymentVersionObject;
+import net.luminis.liq.client.repository.object.GatewayObject;
+import net.luminis.liq.client.repository.object.Group2LicenseAssociation;
+import net.luminis.liq.client.repository.object.GroupObject;
+import net.luminis.liq.client.repository.object.License2GatewayAssociation;
+import net.luminis.liq.client.repository.object.LicenseObject;
+import net.luminis.liq.client.repository.repository.Artifact2GroupAssociationRepository;
+import net.luminis.liq.client.repository.repository.ArtifactRepository;
+import net.luminis.liq.client.repository.repository.DeploymentVersionRepository;
+import net.luminis.liq.client.repository.repository.GatewayRepository;
+import net.luminis.liq.client.repository.repository.Group2LicenseAssociationRepository;
+import net.luminis.liq.client.repository.repository.GroupRepository;
+import net.luminis.liq.client.repository.repository.License2GatewayAssociationRepository;
+import net.luminis.liq.client.repository.repository.LicenseRepository;
+import net.luminis.liq.client.repository.stateful.StatefulGatewayObject;
+import net.luminis.liq.client.repository.stateful.StatefulGatewayRepository;
+import net.luminis.liq.client.repository.stateful.StatefulGatewayObject.ProvisioningState;
+import net.luminis.liq.client.repository.stateful.StatefulGatewayObject.RegistrationState;
+import net.luminis.liq.client.repository.stateful.StatefulGatewayObject.StoreState;
+import net.luminis.liq.http.listener.constants.HttpConstants;
+import net.luminis.liq.log.AuditEvent;
+import net.luminis.liq.log.LogEvent;
+import net.luminis.liq.obr.storage.file.constants.OBRFileStoreConstants;
+import net.luminis.liq.repository.Repository;
+import net.luminis.liq.repository.impl.constants.RepositoryConstants;
+import net.luminis.liq.scheduler.constants.SchedulerConstants;
+import net.luminis.liq.server.log.store.LogStore;
+import net.luminis.liq.test.utils.TestUtils;
+
+import org.apache.felix.dependencymanager.DependencyManager;
+import org.apache.felix.dependencymanager.Service;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.Constants;
+import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.framework.ServiceReference;
+import org.osgi.service.cm.Configuration;
+import org.osgi.service.cm.ConfigurationAdmin;
+import org.osgi.service.event.Event;
+import org.osgi.service.event.EventHandler;
+import org.osgi.service.useradmin.User;
+import org.osgi.util.tracker.ServiceTracker;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.Factory;
+import org.testng.annotations.Test;
+
+public class RepositoryAdminTest implements EventHandler {
+    private BundleContext m_context; /* Injected by dependency manager */
+    private ConfigurationAdmin m_configAdmin; /* Injected by dependency manager */
+    private DependencyManager m_depManager; /* Configured from outside */
+    private volatile RepositoryAdmin m_repositoryAdmin; /* Injected by dependency manager */
+    private volatile ArtifactRepository m_artifactRepository; /* Injected by dependency manager */
+    private volatile Artifact2GroupAssociationRepository m_artifact2groupRepository; /* Injected by dependency manager */
+    private volatile GroupRepository m_groupRepository; /* Injected by dependency manager */
+    private volatile Group2LicenseAssociationRepository m_group2licenseRepository; /* Injected by dependency manager */
+    private volatile LicenseRepository m_licenseRepository; /* Injected by dependency manager */
+    private volatile License2GatewayAssociationRepository m_license2gatewayRepository; /* Injected by dependency manager */
+    private volatile GatewayRepository m_gatewayRepository; /* Injected by dependency manager */
+    private volatile DeploymentVersionRepository m_deploymentVersionRepository; /* Injected by dependency manager */
+    private volatile StatefulGatewayRepository m_statefulGatewayRepository; /* Injected by dependency manager */
+    private volatile LogStore m_auditLogStore; /* Injected by dependency manager */
+
+    /*
+     * TestNG magic
+     */
+    private static Object instance;
+    public RepositoryAdminTest() {
+        if (instance == null) {
+            instance = this;
+        }
+    }
+
+    @Factory
+    public Object[] createInstances() {
+        return new Object[] { instance };
+    }
+
+    @AfterMethod( alwaysRun = true )
+    public void cleanUp() throws IOException, InvalidSyntaxException, InterruptedException {
+        // Simply remove all objects in the repository.
+        clearRepository(m_artifactRepository);
+        clearRepository(m_artifact2groupRepository);
+        clearRepository(m_group2licenseRepository);
+        clearRepository(m_license2gatewayRepository);
+        clearRepository(m_artifactRepository);
+        clearRepository(m_groupRepository);
+        clearRepository(m_licenseRepository);
+        clearRepository(m_gatewayRepository);
+        clearRepository(m_deploymentVersionRepository);
+        m_statefulGatewayRepository.refresh();
+        try {
+            m_repositoryAdmin.logout(true);
+        }
+        catch (Exception ioe) {
+            //ioe.printStackTrace(System.out);
+        }
+    }
+
+    public <T extends RepositoryObject> void clearRepository (ObjectRepository<T> rep) {
+        for (T entity : rep.get()) {
+            rep.remove(entity);
+        }
+        assert rep.get().size() == 0 : "Something went wrong clearing the repository.";
+    }
+
+    /**
+     * Add a bundle, group and license, associate all, remove the group, No associations should be left.
+     * @throws Exception
+     */
+    @Test( groups = { TestUtils.INTEGRATION } )
+    public void testRemoveBundleGroup() throws Exception {
+        final ArtifactObject b1 = createBasicBundleObject("thebundle","1", null);
+        final GroupObject g1 = createBasicGroupObject("thegroup");
+
+        final Artifact2GroupAssociation bg = runAndWaitForEvent(new Callable<Artifact2GroupAssociation>() {
+            public Artifact2GroupAssociation call() throws Exception {
+                return m_artifact2groupRepository.create("(&(" + BundleHelper.KEY_SYMBOLICNAME + "=thebundle)(|("+BundleHelper.KEY_VERSION+">=1)("+BundleHelper.KEY_VERSION+"=<3))(!("+BundleHelper.KEY_VERSION+"=3)))", "(name=thegroup)");
+            }
+        }, false, Artifact2GroupAssociation.TOPIC_ADDED);
+
+        final LicenseObject l1 = createBasicLicenseObject("thelicense");
+
+        final Group2LicenseAssociation gtl = runAndWaitForEvent(new Callable<Group2LicenseAssociation>() {
+            public Group2LicenseAssociation call() throws Exception {
+                return m_group2licenseRepository.create("(name=thegroup)","(name=thelicense)");
+            }
+        }, false, Group2LicenseAssociation.TOPIC_ADDED);
+
+        assert (bg.getLeft().size() == 1) && bg.getLeft().contains(b1) : "The left side of the BG-association should be b1.";
+        assert (bg.getRight().size() == 1) && bg.getRight().contains(g1) : "The right side of the BG-association should be g1.";
+        assert (gtl.getLeft().size() == 1) && gtl.getLeft().contains(g1) : "The left side of the GtL-association should be g1.";
+        assert (gtl.getRight().size() == 1) && gtl.getRight().contains(l1) : "The right side of the GtL-association should be l1.";
+        assert bg.isSatisfied() : "The bundlegroup association should be satisfied.";
+        assert gtl.isSatisfied() : "The group2license association should be satisfied.";
+        assert b1.getGroups().size() == 1 : "Bundle b1 should be associated to one group.";
+        assert l1.getGroups().size() == 1 : "License l1 should be associated to one group.";
+
+        //remove the group
+        runAndWaitForEvent(new Callable<Object>() {
+            public Object call() throws Exception {
+                m_groupRepository.remove(g1);
+                return null;
+            }
+        }, false,Artifact2GroupAssociation.TOPIC_CHANGED, Group2LicenseAssociation.TOPIC_CHANGED);
+
+        assert !gtl.isSatisfied() : "The bundlegroup association shouldn not be satisfied.";
+        assert !bg.isSatisfied() : "The group2license assocation should not be satisfied.";
+
+        assert b1.getGroups().size() == 0 : "Bundle b1 shouldn't be associated to any group, but is associated to " + b1.getGroups();
+        assert l1.getGroups().size() == 0 : "License l1 shouldn't be associated to any group.";
+
+        cleanUp();
+    }
+
+
+    @Test( groups = { TestUtils.INTEGRATION } )
+    public void testAssociationsWithMovingEnpoints () throws Exception {
+        final ArtifactObject b1 = createBasicBundleObject("thebundle", "1", null);
+        final GroupObject g1 = createBasicGroupObject("thegroup");
+        final Artifact2GroupAssociation bg = runAndWaitForEvent(new Callable<Artifact2GroupAssociation>() {
+            public Artifact2GroupAssociation call() throws Exception {
+                Map<String, String> properties = new HashMap<String, String>();
+                properties.put(BundleHelper.KEY_ASSOCIATION_VERSIONSTATEMENT, "[1,3)");
+                return m_artifact2groupRepository.create(b1, properties, g1, null);
+            }
+        }, false, Artifact2GroupAssociation.TOPIC_ADDED);
+
+        assert (bg.getLeft().size() == 1) && bg.getLeft().contains(b1) : "The left side of the association should now be b1; we find " + bg.getLeft().size() + " bundles on the left side of the association.";
+        assert (bg.getRight().size() == 1) && bg.getRight().contains(g1) : "The right side of the association should now be g1.";
+        assert b1.getGroups().get(0) == g1 : "b1 should be assocated with g1";
+        assert g1.getArtifacts().get(0) == b1 : "g1 should be assocated with b1";
+
+        final ArtifactObject b2 = runAndWaitForEvent(new Callable<ArtifactObject>() {
+            public ArtifactObject call() throws Exception {
+                return createBasicBundleObject("thebundle", "2", null);
+            }
+        }, false, Artifact2GroupAssociation.TOPIC_CHANGED);
+
+        assert (bg.getLeft().size() == 1) && !bg.getLeft().contains(b1) : "The left side of the association should no longer be b1; we find " + bg.getLeft().size() + " bundles.";
+        assert (bg.getLeft().size() == 1) && bg.getLeft().contains(b2) : "The left side of the association should now be b2.";
+        assert (bg.getRight().size() == 1) && bg.getRight().contains(g1) : "The right side of the association should now be g1.";
+        assert b1.getGroups().size() == 0 : "b1 should not be associated with any group.";
+        assert b2.getGroups().get(0) == g1 : "b2 should now be assocation with g1";
+        assert g1.getArtifacts().get(0) == b2 : "g1 should be assocation with b2";
+        assert g1.getArtifacts().size() == 1 : "g1 should be associated with one bundle";
+
+        ArtifactObject b3 = createBasicBundleObject("thebundle", "3", null);
+
+        assert (bg.getLeft().size() == 1) && !bg.getLeft().contains(b1) : "The left side of the association should no longer be b1.";
+        assert (bg.getLeft().size() == 1) && bg.getLeft().contains(b2) : "The left side of the association should now be b2.";
+        assert (bg.getLeft().size() == 1) && !bg.getLeft().contains(b3) : "The left side of the association should not be b3.";
+        assert (bg.getRight().size() == 1) && bg.getRight().contains(g1) : "The right side of the association should now be g1.";
+        assert b1.getGroups().size() == 0 : "b1 should not be associated with any group.";
+        assert b2.getGroups().get(0) == g1 : "b2 should now be assocation with g1";
+        assert b3.getGroups().size() == 0 : "b3 should not be associated with any group.";
+        assert g1.getArtifacts().get(0) == b2 : "g1 should be assocation with b2";
+        assert g1.getArtifacts().size() == 1 : "g1 should be associated with one bundle";
+
+        ArtifactObject b15 = createBasicBundleObject("thebundle", "1.5", null);
+
+        assert (bg.getLeft().size() == 1) && !bg.getLeft().contains(b1) : "The left side of the association should no longer be b1.";
+        assert (bg.getLeft().size() == 1) && !bg.getLeft().contains(b15) : "The left side of the association should not be b15.";
+        assert (bg.getLeft().size() == 1) && bg.getLeft().contains(b2) : "The left side of the association should now be b2.";
+        assert (bg.getLeft().size() == 1) && !bg.getLeft().contains(b3) : "The left side of the association should not be b3.";
+        assert (bg.getRight().size() == 1) && bg.getRight().contains(g1) : "The right side of the association should now be g1.";
+        assert b1.getGroups().size() == 0 : "b1 should not be associated with any group.";
+        assert b15.getGroups().size() == 0 : "b15 should not be associated with any group.";
+        assert b2.getGroups().get(0) == g1 : "b2 should now be assocation with g1";
+        assert b3.getGroups().size() == 0 : "b3 should not be associated with any group.";
+        assert g1.getArtifacts().get(0) == b2 : "g1 should be assocation with b2";
+        assert g1.getArtifacts().size() == 1 : "g1 should be associated with one bundle";
+
+        runAndWaitForEvent(new Callable<Object>() {
+            public Object call() throws Exception {
+                m_artifactRepository.remove(b2);
+                return null;
+            }
+        }, false, Artifact2GroupAssociation.TOPIC_CHANGED);
+
+        //note that we cannot test anything for b2: this has been removed, and now has no
+        //defined state.
+        assert (bg.getLeft().size() == 1) && !bg.getLeft().contains(b1) : "The left side of the association should no longer be b1.";
+        assert (bg.getLeft().size() == 1) && bg.getLeft().contains(b15) : "The left side of the association should now be b15.";
+        assert (bg.getLeft().size() == 1) && !bg.getLeft().contains(b3) : "The left side of the association should not be b3.";
+        assert (bg.getRight().size() == 1) && bg.getRight().contains(g1) : "The right side of the association should now be g1.";
+        assert b1.getGroups().size() == 0 : "b1 should not be associated with any group.";
+        assert b15.getGroups().get(0) == g1 : "b15 should now be assocation with g1";
+        assert b3.getGroups().size() == 0 : "b3 should not be associated with any group.";
+        assert g1.getArtifacts().get(0) == b15 : "g1 should be assocation with b15";
+        assert g1.getArtifacts().size() == 1 : "g1 should be associated with one bundle";
+
+        cleanUp();
+    }
+
+    private static final String ENDPOINT = "/AdminRepTest";
+    private static final String HOST = "http://localhost:8080";
+
+    /**
+     * Tests the behavior with logging in and out (with multiple users), and communication
+     * with the server.
+     * @throws Exception
+     */
+    @Test( groups = { TestUtils.INTEGRATION } )
+    public void testRepositoryAdmin() throws Exception {
+        final User user1 = new MockUser("user1");
+        final User user2 = new MockUser("user2");
+
+        startRepositoryService();
+
+        addRepository("storeInstance", "luminis", "store", true);
+        addRepository("gatewayInstance", "luminis", "gateway", true);
+
+        try {
+            m_repositoryAdmin.checkout();
+            assert false : "Without being logged in, it should not be possible to do checkout.";
+        }
+        catch (IllegalStateException ise) {
+            //expected
+        }
+
+        final RepositoryAdminLoginContext loginContext1 = m_repositoryAdmin.createLoginContext(user1);
+        loginContext1.addShopRepository(new URL(HOST + ENDPOINT), "luminis", "store", true);
+        loginContext1.addGatewayRepository(new URL(HOST + ENDPOINT), "luminis", "gateway", true);
+        m_repositoryAdmin.login(loginContext1);
+
+        assert !m_repositoryAdmin.isCurrent() : "When first logging in without checking out, the repository cannot be current.";
+        assert !m_repositoryAdmin.isModified() : "Immediately after login, the repository not is modified.";
+
+        try {
+            m_repositoryAdmin.commit();
+            assert false : "We should not be able to commit before we check something out.";
+        }
+        catch (IllegalStateException e) {
+            // expected
+        }
+
+        runAndWaitForEvent(new Callable<Object>() {
+            public Object call() throws Exception {
+                m_repositoryAdmin.checkout();
+                return null;
+            }
+        }, false, RepositoryAdmin.TOPIC_REFRESH);
+
+        assert m_repositoryAdmin.isCurrent() : "After initial checkout, the repository is current.";
+        assert !m_repositoryAdmin.isModified() : "Immediately after login, the repository cannot be modified.";
+
+        ArtifactObject b1 = runAndWaitForEvent(new Callable<ArtifactObject>() {
+            public ArtifactObject call() throws Exception {
+                return createBasicBundleObject("bundle1");
+            }
+        }, false, ArtifactObject.TOPIC_ADDED, RepositoryAdmin.TOPIC_STATUSCHANGED);
+
+        assert m_repositoryAdmin.isCurrent() : "After initial checkout, the repository is current.";
+        assert m_repositoryAdmin.isModified() : "We have added a bundle, so the repository is modified.";
+        assert m_artifactRepository.get().size() == 1;
+        assert m_repositoryAdmin.getWorkingState(b1).equals(WorkingState.New) : "We expect the working state of our bundle to be New, but it is " + m_repositoryAdmin.getWorkingState(b1);
+        assert m_repositoryAdmin.getNumberWithWorkingState(ArtifactObject.class, WorkingState.New) == 1 : "We expect one bundle object in working state New, but we find "+ m_repositoryAdmin.getNumberWithWorkingState(ArtifactObject.class, WorkingState.New);
+        assert m_repositoryAdmin.getNumberWithWorkingState(ArtifactObject.class, WorkingState.Changed) == 0 : "We expect 0 bundle object in working state Changed, but we find "+ m_repositoryAdmin.getNumberWithWorkingState(ArtifactObject.class, WorkingState.Changed);
+        assert m_repositoryAdmin.getNumberWithWorkingState(ArtifactObject.class, WorkingState.Unchanged) == 0 : "We expect 0 bundle object in working state New, but we find "+ m_repositoryAdmin.getNumberWithWorkingState(ArtifactObject.class, WorkingState.Changed);
+
+        runAndWaitForEvent(new Callable<Object>() {
+            public Object call() throws Exception {
+                m_repositoryAdmin.logout(false);
+                return null;
+            }
+        }, false, RepositoryAdmin.TOPIC_LOGOUT);
+
+        cleanUp();
+
+        assert m_artifactRepository.get().size() == 0;
+
+        runAndWaitForEvent(new Callable<Object>() {
+            public Object call() throws Exception {
+                m_repositoryAdmin.login(loginContext1);
+                return null;
+            }
+        }, false, RepositoryAdmin.TOPIC_LOGIN);
+
+        assert m_repositoryAdmin.isCurrent() : "There has not been another commit in between, so we are still current.";
+        assert m_repositoryAdmin.isModified() : "We have made changes since the last commit, so the repository must be modified.";
+        assert m_artifactRepository.get().size() == 1;
+        assert m_repositoryAdmin.getWorkingState(b1).equals(WorkingState.New) : "We expect the working state of our bundle to be New, but it is " + m_repositoryAdmin.getWorkingState(b1);
+        assert m_repositoryAdmin.getNumberWithWorkingState(ArtifactObject.class, WorkingState.New) == 1 : "We expect one bundle object in working state New, but we find "+ m_repositoryAdmin.getNumberWithWorkingState(ArtifactObject.class, WorkingState.New);
+        assert m_repositoryAdmin.getNumberWithWorkingState(ArtifactObject.class, WorkingState.Changed) == 0 : "We expect 0 bundle object in working state Changed, but we find "+ m_repositoryAdmin.getNumberWithWorkingState(ArtifactObject.class, WorkingState.Changed);
+        assert m_repositoryAdmin.getNumberWithWorkingState(ArtifactObject.class, WorkingState.Unchanged) == 0 : "We expect 0 bundle object in working state New, but we find "+ m_repositoryAdmin.getNumberWithWorkingState(ArtifactObject.class, WorkingState.Changed);
+
+        m_repositoryAdmin.commit();
+
+        assert m_repositoryAdmin.isCurrent() : "After a commit, the repository must be current.";
+        assert !m_repositoryAdmin.isModified() : "After a commit, the repository cannot be modified.";
+
+        runAndWaitForEvent(new Callable<Object>() {
+            public Object call() throws Exception {
+                m_repositoryAdmin.logout(false);
+                return null;
+            }
+        }, false, RepositoryAdmin.TOPIC_LOGOUT);
+
+        cleanUp();
+
+        final RepositoryAdminLoginContext loginContext2 = m_repositoryAdmin.createLoginContext(user2);
+        loginContext2.addShopRepository(new URL(HOST + ENDPOINT), "luminis", "store", true);
+        loginContext2.addGatewayRepository(new URL(HOST + ENDPOINT), "luminis", "gateway", true);
+
+        runAndWaitForEvent(new Callable<Object>() {
+            public Object call() throws Exception {
+                m_repositoryAdmin.login(loginContext2);
+                return null;
+            }
+        }, false, RepositoryAdmin.TOPIC_LOGIN);
+
+        assert m_artifactRepository.get().size() == 0;
+
+        runAndWaitForEvent(new Callable<Object>() {
+            public Object call() throws Exception {
+                m_repositoryAdmin.checkout();
+                return null;
+            }
+        }, false, RepositoryAdmin.TOPIC_REFRESH);
+
+        assert m_artifactRepository.get().size() == 1 : "We expect to find 1 bundle after checkout, but we find " + m_artifactRepository.get().size();
+        assert m_repositoryAdmin.isCurrent() : "After a checkout, without changing anything, the repository must be current.";
+        assert !m_repositoryAdmin.isModified() : "After a checkout, without changing anything, the repository cannot be modified.";
+
+        ArtifactObject b2 = runAndWaitForEvent(new Callable<ArtifactObject>() {
+            public ArtifactObject call() throws Exception {
+                return createBasicBundleObject("bundle2");
+            }
+        }, false, ArtifactObject.TOPIC_ADDED, RepositoryAdmin.TOPIC_STATUSCHANGED);
+
+        assert m_artifactRepository.get().size() == 2;
+        assert m_repositoryAdmin.isCurrent() : "After changing something in memory without flushing it, the repository still is current.";
+        assert m_repositoryAdmin.isModified() : "We have added something, so the repository is modified.";
+        assert m_repositoryAdmin.getWorkingState(b1).equals(WorkingState.Unchanged) : "We expect the working state of our bundle1 to be Unchanged, but it is " + m_repositoryAdmin.getWorkingState(b1);
+        assert m_repositoryAdmin.getWorkingState(b2).equals(WorkingState.New) : "We expect the working state of our bundle2 to be New, but it is " + m_repositoryAdmin.getWorkingState(b1);
+        assert m_repositoryAdmin.getNumberWithWorkingState(ArtifactObject.class, WorkingState.New) == 1 : "We expect one bundle object in working state New, but we find "+ m_repositoryAdmin.getNumberWithWorkingState(ArtifactObject.class, WorkingState.New);
+        assert m_repositoryAdmin.getNumberWithWorkingState(ArtifactObject.class, WorkingState.Changed) == 0 : "We expect 0 bundle object in working state Changed, but we find "+ m_repositoryAdmin.getNumberWithWorkingState(ArtifactObject.class, WorkingState.Changed);
+        assert m_repositoryAdmin.getNumberWithWorkingState(ArtifactObject.class, WorkingState.Unchanged) == 1 : "We expect 1 bundle object in working state New, but we find "+ m_repositoryAdmin.getNumberWithWorkingState(ArtifactObject.class, WorkingState.Changed);
+
+        runAndWaitForEvent(new Callable<Object>() {
+            public Object call() throws Exception {
+                m_repositoryAdmin.logout(false);
+                return null;
+            }
+        }, false, RepositoryAdmin.TOPIC_LOGOUT);
+
+        runAndWaitForEvent(new Callable<Object>() {
+            public Object call() throws Exception {
+                m_repositoryAdmin.login(loginContext1);
+                return null;
+            }
+        }, false, RepositoryAdmin.TOPIC_LOGIN);
+
+        assert m_artifactRepository.get().size() == 1 : "We expect 1 item in the bundle repository, in stead of " + m_artifactRepository.get().size();
+
+        runAndWaitForEvent(new Callable<Object>() {
+            public Object call() throws Exception {
+                m_repositoryAdmin.logout(false);
+                return null;
+            }
+        }, false, RepositoryAdmin.TOPIC_LOGOUT);
+
+        runAndWaitForEvent(new Callable<Object>() {
+            public Object call() throws Exception {
+                m_repositoryAdmin.login(loginContext2);
+                return null;
+            }
+        }, false, RepositoryAdmin.TOPIC_LOGIN);
+
+        assert m_artifactRepository.get().size() == 2 : "We expect 2 items in the bundle repository, in stead of " + m_artifactRepository.get().size();
+
+        runAndWaitForEvent(new Callable<Object>() {
+            public Object call() throws Exception {
+                m_repositoryAdmin.revert();
+                return null;
+            }
+        }, false, RepositoryAdmin.TOPIC_REFRESH, RepositoryAdmin.TOPIC_STATUSCHANGED);
+
+        assert m_artifactRepository.get().size() == 1 : "We expect 1 item in the bundle repository, in stead of " + m_artifactRepository.get().size();
+
+        try {
+            removeAllRepositories();
+        }
+        catch (Exception e) {
+            // Not much we can do...
+            e.printStackTrace(System.err);
+        }
+
+        cleanUp();
+    }
+
+    @Test( groups = { TestUtils.INTEGRATION } )
+    public void testAutoApprove() throws Exception {
+        User user = new MockUser("user");
+
+        startRepositoryService();
+
+        addRepository("storeInstance", "luminis", "store", true);
+        addRepository("gatewayInstance", "luminis", "gateway", true);
+        addRepository("deploymentInstance", "luminis", "deployment", true);
+
+        RepositoryAdminLoginContext loginContext = m_repositoryAdmin.createLoginContext(user);
+        loginContext.addShopRepository(new URL(HOST + ENDPOINT), "luminis", "store", true);
+        loginContext.addGatewayRepository(new URL(HOST + ENDPOINT), "luminis", "gateway", true);
+        loginContext.addDeploymentRepository(new URL(HOST + ENDPOINT), "luminis", "deployment", true);
+        m_repositoryAdmin.login(loginContext);
+
+        runAndWaitForEvent(new Callable<Object>() {
+            public Object call() throws Exception {
+                createBasicGatewayObject("testAutoApproveGateway");
+                return null;
+            }
+        }, false, GatewayObject.TOPIC_ADDED, StatefulGatewayObject.TOPIC_ADDED);
+
+        final StatefulGatewayObject sgo = m_statefulGatewayRepository.get(m_context.createFilter("(" + GatewayObject.KEY_ID + "=" + "testAutoApproveGateway)")).get(0);
+
+        // Set up some deployment information for the gateway.
+        final GroupObject g = runAndWaitForEvent(new Callable<GroupObject>() {
+            public GroupObject call() throws Exception {
+                ArtifactObject b = createBasicBundleObject("myBundle", "1.0", null);
+                GroupObject g = createBasicGroupObject("myGroup");
+                LicenseObject l = createBasicLicenseObject("myLicense");
+                m_artifact2groupRepository.create(b, g);
+                m_group2licenseRepository.create(g, l);
+                m_license2gatewayRepository.create(l, sgo.getGatewayObject());
+                return g;
+            }
+        }, false, ArtifactObject.TOPIC_ADDED, GroupObject.TOPIC_ADDED, LicenseObject.TOPIC_ADDED,
+                  Artifact2GroupAssociation.TOPIC_ADDED, Group2LicenseAssociation.TOPIC_ADDED,
+                  License2GatewayAssociation.TOPIC_ADDED, StatefulGatewayObject.TOPIC_STATUS_CHANGED);
+
+        assert sgo.needsApprove() : "We added some deployment information, so the gateway should need approval.";
+
+        sgo.setAutoApprove(true);
+
+        assert sgo.needsApprove() : "Turning on the autoapprove should not automatically approve whatever was waiting.";
+
+        runAndWaitForEvent(new Callable<Object>() {
+            public Object call() throws Exception {
+                sgo.approve();
+                return null;
+            }
+        }, false, StatefulGatewayObject.TOPIC_STATUS_CHANGED);
+
+        assert !sgo.needsApprove() : "We approved the new version by hand, so we should not need approval.";
+
+        runAndWaitForEvent(new Callable<Object>() {
+            public Object call() throws Exception {
+                ArtifactObject b = createBasicBundleObject("myBundle2", "1.0", null);
+                m_artifact2groupRepository.create(b, g);
+                return null;
+            }
+        }, false, ArtifactObject.TOPIC_ADDED, Artifact2GroupAssociation.TOPIC_ADDED, StatefulGatewayObject.TOPIC_STATUS_CHANGED, StatefulGatewayObject.TOPIC_STATUS_CHANGED);
+
+      assert !sgo.needsApprove() : "With autoapprove on, adding new deployment information should still not need approval (at least, after the two CHANGED events).";
+
+        runAndWaitForEvent(new Callable<Object>() {
+            public Object call() throws Exception {
+                m_statefulGatewayRepository.unregister(sgo.getID());
+                return null;
+            }
+        }, false, StatefulGatewayObject.TOPIC_STATUS_CHANGED, StatefulGatewayObject.TOPIC_REMOVED);
+
+        try {
+            removeAllRepositories();
+        }
+        catch (Exception e) {
+            // Not much we can do...
+            e.printStackTrace(System.err);
+        }
+        cleanUp();
+    }
+
+    @Test( groups = { TestUtils.INTEGRATION } )
+    public void testStateful() throws Exception {
+        User user = new MockUser("user");
+
+        startRepositoryService();
+
+        addRepository("storeInstance", "luminis", "store", true);
+        addRepository("gatewayInstance", "luminis", "gateway", true);
+        addRepository("deploymentInstance", "luminis", "deployment", true);
+
+        RepositoryAdminLoginContext loginContext = m_repositoryAdmin.createLoginContext(user);
+        loginContext.addShopRepository(new URL(HOST + ENDPOINT), "luminis", "store", true);
+        loginContext.addGatewayRepository(new URL(HOST + ENDPOINT), "luminis", "gateway", true);
+        loginContext.addDeploymentRepository(new URL(HOST + ENDPOINT), "luminis", "deployment", true);
+        m_repositoryAdmin.login(loginContext);
+
+        /*
+         * First, test some functionality without auditlog data.
+         * I would prefer to test without an auditlog present at all, but that
+         * is not so easily done.
+         */
+        testStatefulCreateRemove();
+        testStatefulSetAutoApprove();
+        testStatefulApprove();
+
+        testStatefulAuditlog();
+        testStatefulAuditAndRegister();
+        testStrangeNamesInGateways();
+
+        try {
+            removeAllRepositories();
+        }
+        catch (Exception e) {
+            // Not much we can do...
+            e.printStackTrace(System.err);
+        }
+
+        cleanUp();
+    }
+
+    private void testStatefulSetAutoApprove() throws Exception {
+
+        // register gateway with
+        final Map<String, String> attr = new HashMap<String, String>();
+        attr.put(GatewayObject.KEY_ID, "a_gateway");
+        attr.put(GatewayObject.KEY_AUTO_APPROVE, String.valueOf(true));
+        final Map<String, String> tags = new HashMap<String, String>();
+
+        final StatefulGatewayObject sgo = runAndWaitForEvent(new Callable<StatefulGatewayObject>() {
+            public StatefulGatewayObject call() throws Exception {
+                return m_statefulGatewayRepository.preregister(attr, tags);
+            }
+        }, false, GatewayObject.TOPIC_ADDED, StatefulGatewayObject.TOPIC_ADDED);
+
+        assert m_gatewayRepository.get().size() == 1 : "We expect to find exactly one gateway in the repository, but we find " + m_gatewayRepository.get().size();
+        assert m_statefulGatewayRepository.get().size() == 1 : "We expect to find exactly one stateful gateway in the repository, but we find " + m_statefulGatewayRepository.get().size();
+
+        assert sgo.getAutoApprove() : "The gateway should have auto approved value: true but got: false.";
+
+        sgo.setAutoApprove(false);
+
+        assert !sgo.getAutoApprove() : "The gateway should have auto approved value: false but got: true.";
+
+        //clean up
+        runAndWaitForEvent(new Callable<Object>() {
+            public Object call() throws Exception {
+                m_statefulGatewayRepository.unregister(sgo.getID());
+                return null;
+            }
+        }, false, GatewayObject.TOPIC_REMOVED, StatefulGatewayObject.TOPIC_REMOVED);
+    }
+
+    private void testStatefulCreateRemove() throws Exception {
+        final Map<String, String> attr = new HashMap<String, String>();
+        attr.put(GatewayObject.KEY_ID, "myNewGateway1");
+        final Map<String, String> tags = new HashMap<String, String>();
+
+        try {
+            m_statefulGatewayRepository.create(attr, tags);
+            assert false : "Creating a stateful gateway repository should not be allowed.";
+        }
+        catch (UnsupportedOperationException uoe) {
+            // expected
+        }
+
+        final StatefulGatewayObject sgo = runAndWaitForEvent(new Callable<StatefulGatewayObject>() {
+            public StatefulGatewayObject call() throws Exception {
+                return m_statefulGatewayRepository.preregister(attr, tags);
+            }
+        }, false, GatewayObject.TOPIC_ADDED, StatefulGatewayObject.TOPIC_ADDED);
+
+        assert m_gatewayRepository.get().size() == 1 : "We expect to find exactly one gateway in the repository, but we find " + m_gatewayRepository.get().size();
+        assert m_statefulGatewayRepository.get().size() == 1 : "We expect to find exactly one stateful gateway in the repository, but we find " + m_statefulGatewayRepository.get().size();
+
+        try {
+            m_statefulGatewayRepository.remove(sgo);
+            assert false : "Deleting a stateful gateway repositoy should not be allowed.";
+        }
+        catch (UnsupportedOperationException uoe) {
+            // expected
+        }
+
+        assert m_gatewayRepository.get().size() == 1 : "We expect to find exactly one gateway in the repository, but we find " + m_gatewayRepository.get().size();
+        assert m_statefulGatewayRepository.get().size() == 1 : "We expect to find exactly one stateful gateway in the repository, but we find " + m_statefulGatewayRepository.get().size();
+
+        runAndWaitForEvent(new Callable<Object>() {
+            public Object call() throws Exception {
+                m_statefulGatewayRepository.unregister(sgo.getID());
+                return null;
+            }
+        }, false, GatewayObject.TOPIC_REMOVED, StatefulGatewayObject.TOPIC_REMOVED);
+
+        assert m_gatewayRepository.get().size() == 0 : "We expect to find no gateway in the repository, but we find " + m_gatewayRepository.get().size();
+        assert m_statefulGatewayRepository.get().size() == 0 : "We expect to find no stateful gateway in the repository, but we find " + m_statefulGatewayRepository.get().size();
+    }
+
+    private void testStatefulApprove() throws Exception {
+        final Map<String, String> attr = new HashMap<String, String>();
+        attr.put(GatewayObject.KEY_ID, "myNewGateway2");
+        final Map<String, String> tags = new HashMap<String, String>();
+        final StatefulGatewayObject sgo = runAndWaitForEvent(new Callable<StatefulGatewayObject>() {
+            public StatefulGatewayObject call() throws Exception {
+                return m_statefulGatewayRepository.preregister(attr, tags);
+            }
+        }, false, GatewayObject.TOPIC_ADDED, StatefulGatewayObject.TOPIC_ADDED);
+
+        assert !sgo.needsApprove() : "Without any deployment versions, and no information in the shop, we should not need to approve.";
+        assert sgo.getRegistrationState().equals(RegistrationState.Registered) : "We expect the registration state to be Registered, but it is " + sgo.getRegistrationState();
+        assert sgo.getStoreState().equals(StoreState.New) : "We expect the registration state to be New, but it is " + sgo.getStoreState();
+        assert sgo.getCurrentVersion().equals(StatefulGatewayObject.UNKNOWN_VERSION);
+
+        final ArtifactObject b11 = createBasicBundleObject("bundle1", "1", null);
+
+        GroupObject g1 = createBasicGroupObject("group1");
+        GroupObject g2 = createBasicGroupObject("group2"); // note that this group is not associated to a bundle.
+
+        createDynamicBundle2GroupAssociation(b11, g1);
+
+        final LicenseObject l1 = createBasicLicenseObject("license1");
+
+        m_group2licenseRepository.create(g1, l1);
+        m_group2licenseRepository.create(g2, l1);
+
+        runAndWaitForEvent(new Callable<License2GatewayAssociation>() {
+                public License2GatewayAssociation call() throws Exception {
+                    return m_license2gatewayRepository.create(l1, sgo.getGatewayObject());
+                }
+            }, false, License2GatewayAssociation.TOPIC_ADDED, StatefulGatewayObject.TOPIC_STATUS_CHANGED);
+
+        assert sgo.needsApprove() : "We added information that influences our gateway, so we should need to approve it.";
+        assert sgo.getRegistrationState().equals(RegistrationState.Registered) : "We expect the registration state to be Registered, but it is " + sgo.getRegistrationState();
+        assert sgo.getStoreState().equals(StoreState.Unapproved) : "We expect the registration state to be Unapproved, but it is " + sgo.getStoreState();
+        assert sgo.getArtifactsFromShop().length == 1 : "According to the shop, this gateway needs 1 bundle, but it states we need " + sgo.getArtifactsFromShop().length;
+        assert sgo.getArtifactsFromDeployment().length == 0 : "According to the deployment, this gateway needs 0 bundles, but it states we need " + sgo.getArtifactsFromDeployment().length;
+        assert sgo.getCurrentVersion().equals(StatefulGatewayObject.UNKNOWN_VERSION);
+
+        runAndWaitForEvent(new Callable<Object>() {
+            public Object call() throws Exception {
+                createBasicDeploymentVersionObject("myNewGateway2", "1", b11);
+                return null;
+            }
+        }, false, DeploymentVersionObject.TOPIC_ADDED, StatefulGatewayObject.TOPIC_STATUS_CHANGED);
+
+        assert !sgo.needsApprove() : "We manually created a deployment version that reflects the shop, so no approval should be necessary.";
+        assert sgo.getRegistrationState().equals(RegistrationState.Registered) : "We expect the registration state to be Registered, but it is " + sgo.getRegistrationState();
+        assert sgo.getStoreState().equals(StoreState.Approved) : "We expect the registration state to be Approved, but it is " + sgo.getStoreState();
+        assert sgo.getArtifactsFromShop().length == 1 : "According to the shop, this gateway needs 1 bundle, but it states we need " + sgo.getArtifactsFromShop().length;
+        assert sgo.getArtifactsFromDeployment().length == 1 : "According to the deployment, this gateway needs 1 bundles, but it states we need " + sgo.getArtifactsFromDeployment().length;
+
+        runAndWaitForEvent(new Callable<ArtifactObject>() {
+            public ArtifactObject call() throws Exception {
+                return createBasicBundleObject("bundle1", "2", null);
+            }
+        }, false, ArtifactObject.TOPIC_ADDED, Artifact2GroupAssociation.TOPIC_CHANGED, StatefulGatewayObject.TOPIC_STATUS_CHANGED);
+
+        assert sgo.needsApprove() : "We added a new version of a bundle that is used by the gateway, so approval should be necessary.";
+        assert sgo.getRegistrationState().equals(RegistrationState.Registered) : "We expect the registration state to be Registered, but it is " + sgo.getRegistrationState();
+        assert sgo.getStoreState().equals(StoreState.Unapproved) : "We expect the registration state to be Unapproved, but it is " + sgo.getStoreState();
+        assert sgo.getArtifactsFromShop().length == 1 : "According to the shop, this gateway needs 1 bundle, but it states we need " + sgo.getArtifactsFromShop().length;
+        assert sgo.getArtifactsFromShop()[0].getURL().equals("http://bundle1-2") : "The shop should tell use we need bundle URL 'bundle1-2', but it tells us we need " + sgo.getArtifactsFromShop()[0].getURL();
+        assert sgo.getArtifactsFromDeployment().length == 1 : "According to the deployment, this gateway needs 1 bundles, but it states we need " + sgo.getArtifactsFromDeployment().length;
+        assert sgo.getArtifactsFromDeployment()[0].getUrl().equals("http://bundle1-1") : "The deployment should tell use we need bundle URL 'bundle1-1', but it tells us we need " + sgo.getArtifactsFromDeployment()[0].getUrl();
+        assert sgo.getCurrentVersion().equals("1");
+
+        final String newVersion = runAndWaitForEvent(new Callable<String>() {
+            public String call() throws Exception {
+                return sgo.approve();
+            }
+        }, false, DeploymentVersionObject.TOPIC_ADDED, StatefulGatewayObject.TOPIC_STATUS_CHANGED);
+
+        assert !sgo.needsApprove() : "Immediately after approval, no approval is necessary.";
+        assert sgo.getRegistrationState().equals(RegistrationState.Registered) : "We expect the registration state to be Registered, but it is " + sgo.getRegistrationState();
+        assert sgo.getStoreState().equals(StoreState.Approved) : "We expect the registration state to be Approved, but it is " + sgo.getStoreState();
+        assert sgo.getArtifactsFromShop().length == 1 : "According to the shop, this gateway needs 1 bundle, but it states we need " + sgo.getArtifactsFromShop().length;
+        assert sgo.getArtifactsFromShop()[0].getURL().equals("http://bundle1-2") : "The shop should tell use we need bundle URL 'bundle1-2', but it tells us we need " + sgo.getArtifactsFromShop()[0].getURL();
+        assert sgo.getArtifactsFromDeployment().length == 1 : "According to the deployment, this gateway needs 1 bundles, but it states we need " + sgo.getArtifactsFromDeployment().length;
+        assert sgo.getArtifactsFromShop()[0].getURL().equals("http://bundle1-2") : "Deployment should tell use we need bundle URL 'bundle1-2', but it tells us we need " + sgo.getArtifactsFromShop()[0].getURL();
+        assert m_deploymentVersionRepository.get().size() == 2 : "We expect two deployment versions, but we find " + m_deploymentVersionRepository.get().size();
+        assert sgo.getCurrentVersion().equals(newVersion);
+
+        // clean up this object ourselves; we cannot rely on cleanUp() in this case.
+        runAndWaitForEvent(new Callable<Object>() {
+            public Object call() throws Exception {
+                m_statefulGatewayRepository.unregister(sgo.getID());
+                return null;
+            }
+        }, false, GatewayObject.TOPIC_REMOVED, StatefulGatewayObject.TOPIC_REMOVED);
+
+        assert m_statefulGatewayRepository.get().size() == 0;
+    }
+
+    private void testStrangeNamesInGateways() throws InvalidSyntaxException, IOException {
+        List<LogEvent> events = new ArrayList<LogEvent>();
+        Properties props = new Properties();
+
+        // add a gateway with a weird name.
+        events.add(new LogEvent(":)", 1, 1, 1, AuditEvent.FRAMEWORK_STARTED, props));
+        //fill auditlog; no install data
+        m_auditLogStore.put(events);
+
+        //see presence of sgo
+        int sgrSizeBefore = m_statefulGatewayRepository.get().size();
+        m_statefulGatewayRepository.refresh();
+        assert m_statefulGatewayRepository.get().size() == sgrSizeBefore + 1 : "After refresh, we expect " + (sgrSizeBefore + 1) + " gateway based on auditlogdata, but we find " + m_statefulGatewayRepository.get().size();
+        StatefulGatewayObject sgo = findStatefulGateway(":)");
+        sgo.register();
+        assert sgo.getRegistrationState().equals(RegistrationState.Registered) : "After registring our gateway, we assume it to be registered.";
+        assert sgo.getProvisioningState().equals(ProvisioningState.Idle) : "We expect our object's provisioning state to be Idle, but it is " + m_statefulGatewayRepository.get().get(0).getProvisioningState();
+
+    }
+
+
+    private void testStatefulAuditlog() throws IOException, InvalidSyntaxException {
+        List<LogEvent> events = new ArrayList<LogEvent>();
+        Properties props = new Properties();
+        events.add(new LogEvent("myGateway", 1, 1, 1, AuditEvent.FRAMEWORK_STARTED, props));
+        //fill auditlog; no install data
+        m_auditLogStore.put(events);
+
+        //see presence of sgo
+        assert m_statefulGatewayRepository.get().size() == 0 : "Before audit log refresh, we expect nothing in the stateful repository, but we find " + m_statefulGatewayRepository.get().size();
+        m_statefulGatewayRepository.refresh();
+        assert m_statefulGatewayRepository.get().size() == 1 : "After refresh, we expect 1 gateway based on auditlogdata, but we find " + m_statefulGatewayRepository.get().size();
+        StatefulGatewayObject sgo = m_statefulGatewayRepository.get().get(0);
+        assert sgo.getProvisioningState().equals(ProvisioningState.Idle) : "We expect our object's provisioning state to be Idle, but it is " + m_statefulGatewayRepository.get().get(0).getProvisioningState();
+
+        //fill auditlog with complete-data
+        events = new ArrayList<LogEvent>();
+        props = new Properties();
+        props.put(AuditEvent.KEY_NAME, "mypackage");
+        props.put(AuditEvent.KEY_VERSION, "123");
+        events.add(new LogEvent("myGateway", 1, 2, 2, AuditEvent.DEPLOYMENTCONTROL_INSTALL, props));
+        m_auditLogStore.put(events);
+        m_statefulGatewayRepository.refresh();
+
+        assert sgo.getLastInstallVersion().equals("123") : "Our last install version should be 123, but it is " + sgo.getLastInstallVersion();
+        assert sgo.getProvisioningState().equals(ProvisioningState.InProgress)  : "We expect our object's provisioning state to be InProgress, but it is " + sgo.getProvisioningState();
+
+        //fill auditlog with install data
+        events = new ArrayList<LogEvent>();
+        props = new Properties();
+        props.put(AuditEvent.KEY_NAME, "mypackage");
+        props.put(AuditEvent.KEY_VERSION, "123");
+        props.put(AuditEvent.KEY_SUCCESS, "false");
+        events.add(new LogEvent("myGateway", 1, 3, 3, AuditEvent.DEPLOYMENTADMIN_COMPLETE, props));
+        m_auditLogStore.put(events);
+        m_statefulGatewayRepository.refresh();
+
+        assert sgo.getLastInstallVersion().equals("123") : "Our last install version should be 123, but it is " + sgo.getLastInstallVersion();
+        assert sgo.getProvisioningState().equals(ProvisioningState.Failed)  : "We expect our object's provisioning state to be Failed, but it is " + sgo.getProvisioningState();
+        assert !sgo.getLastInstallSuccess() : "Our last install was not successful, but according to the sgo it was.";
+
+        sgo.acknowledgeInstallVersion("123");
+        assert sgo.getProvisioningState().equals(ProvisioningState.Idle)  : "We expect our object's provisioning state to be Idle, but it is " + sgo.getProvisioningState();
+
+        //add another install event.
+        events = new ArrayList<LogEvent>();
+        props = new Properties();
+        props.put(AuditEvent.KEY_NAME, "mypackage");
+        props.put(AuditEvent.KEY_VERSION, "124");
+        events.add(new LogEvent("myGateway", 1, 4, 4, AuditEvent.DEPLOYMENTCONTROL_INSTALL, props));
+        m_auditLogStore.put(events);
+        m_statefulGatewayRepository.refresh();
+
+        assert sgo.getLastInstallVersion().equals("124") : "Our last install version should be 124, but it is " + sgo.getLastInstallVersion();
+        assert sgo.getProvisioningState().equals(ProvisioningState.InProgress)  : "We expect our object's provisioning state to be InProgress, but it is " + sgo.getProvisioningState();
+
+        //fill auditlog with install data
+        events = new ArrayList<LogEvent>();
+        props = new Properties();
+        props.put(AuditEvent.KEY_NAME, "mypackage");
+        props.put(AuditEvent.KEY_VERSION, "124");
+        props.put(AuditEvent.KEY_SUCCESS, "true");
+        events.add(new LogEvent("myGateway", 1, 5, 5, AuditEvent.DEPLOYMENTADMIN_COMPLETE, props));
+        m_auditLogStore.put(events);
+        m_statefulGatewayRepository.refresh();
+
+        assert sgo.getLastInstallVersion().equals("124") : "Our last install version should be 124, but it is " + sgo.getLastInstallVersion();
+        assert sgo.getProvisioningState().equals(ProvisioningState.OK)  : "We expect our object's provisioning state to be OK, but it is " + sgo.getProvisioningState();
+        assert sgo.getLastInstallSuccess() : "Our last install was successful, but according to the sgo it was not.";
+
+        sgo.acknowledgeInstallVersion("124");
+        assert sgo.getProvisioningState().equals(ProvisioningState.Idle)  : "We expect our object's provisioning state to be Idle, but it is " + sgo.getProvisioningState();
+    }
+
+    private void testStatefulAuditAndRegister() throws Exception {
+        //preregister gateway
+        final Map<String, String> attr = new HashMap<String, String>();
+        attr.put(GatewayObject.KEY_ID, "myNewGateway3");
+        final Map<String, String> tags = new HashMap<String, String>();
+
+        final StatefulGatewayObject sgo1 = runAndWaitForEvent(new Callable<StatefulGatewayObject>() {
+            public StatefulGatewayObject call() throws Exception {
+                return m_statefulGatewayRepository.preregister(attr, tags);
+            }
+        }, false, GatewayObject.TOPIC_ADDED, StatefulGatewayObject.TOPIC_ADDED);
+
+        //do checks
+        assert sgo1.isRegistered() : "We just preregistered a gateway, so it should be registered.";
+
+        //add auditlog data
+        List<LogEvent> events = new ArrayList<LogEvent>();
+        Properties props = new Properties();
+        events.add(new LogEvent("myNewGateway3", 1, 1, 1, AuditEvent.FRAMEWORK_STARTED, props));
+        m_auditLogStore.put(events);
+        m_statefulGatewayRepository.refresh();
+
+        //do checks
+        assert sgo1.isRegistered() : "Adding auditlog data for a gateway does not influence its isRegistered().";
+        try {
+            sgo1.getGatewayObject();
+        }
+        catch (IllegalStateException ise) {
+            assert false : "We should be able to get sgo1's gatewayObject.";
+        }
+
+        //add auditlog data for other gateway
+        events = new ArrayList<LogEvent>();
+        props = new Properties();
+        events.add(new LogEvent("myNewGateway4", 1, 1, 1, AuditEvent.FRAMEWORK_STARTED, props));
+        m_auditLogStore.put(events);
+        runAndWaitForEvent(new Callable<Object>() {
+            public Object call() throws Exception {
+                m_statefulGatewayRepository.refresh();
+                return false;
+            }
+        }, false, StatefulGatewayObject.TOPIC_ADDED);
+        final StatefulGatewayObject sgo2 = findStatefulGateway("myNewGateway4");
+
+        //do checks
+        assert sgo1.isRegistered() : "Adding auditlog data for a gateway does not influence its isRegistered().";
+        try {
+            sgo1.getGatewayObject();
+        }
+        catch (IllegalStateException ise) {
+            assert false : "We should be able to get sgo1's gatewayObject.";
+        }
+        assert !sgo2.isRegistered() : "sgo2 is only found in the auditlog, so it cannot be in registered.";
+        try {
+            sgo2.getGatewayObject();
+            assert false : "We should not be able to get sgo2's gatewayObject.";
+        }
+        catch (IllegalStateException ise) {
+            // expected
+        }
+
+        //remove original gateway
+        runAndWaitForEvent(new Callable<Object>() {
+            public Object call() throws Exception {
+                m_statefulGatewayRepository.unregister(sgo1.getID());
+                return null;
+            }
+        }, false, GatewayObject.TOPIC_REMOVED, StatefulGatewayObject.TOPIC_STATUS_CHANGED);
+
+        //do checks
+        assert !sgo1.isRegistered() : "sgo1 is now only found in the auditlog, so it cannot be registered.";
+        try {
+            sgo1.getGatewayObject();
+            assert false : "We should not be able to get sgo1's gatewayObject.";
+        }
+        catch (IllegalStateException ise) {
+            //expected
+        }
+        assert !sgo2.isRegistered() : "sgo2 is only found in the auditlog, so it cannot be in registered.";
+        try {
+            sgo2.getGatewayObject();
+            assert false : "We should not be able to get sgo2's gatewayObject.";
+        }
+        catch (IllegalStateException ise) {
+            // expected
+        }
+
+        //register second gateway
+        runAndWaitForEvent(new Callable<Object>() {
+            public Object call() throws Exception {
+                sgo2.register();
+                return null;
+            }
+        }, false, GatewayObject.TOPIC_ADDED, StatefulGatewayObject.TOPIC_STATUS_CHANGED);
+
+        //do checks
+        assert !sgo1.isRegistered() : "sgo1 is now only found in the auditlog, so it cannot be in registered.";
+        try {
+            sgo1.getGatewayObject();
+            assert false : "We should not be able to get sgo1's gatewayObject.";
+        }
+        catch (IllegalStateException ise) {
+            //expected
+        }
+        assert sgo2.isRegistered() : "sgo2 has been registered.";
+        try {
+            sgo2.getGatewayObject();
+        }
+        catch (IllegalStateException ise) {
+            assert false : "We should be able to get sgo2's gatewayObject.";
+        }
+
+        int nrRegistered = m_statefulGatewayRepository.get(m_context.createFilter("(" + StatefulGatewayObject.KEY_REGISTRATION_STATE + "=" + RegistrationState.Registered + ")")).size();
+        assert nrRegistered == 1 : "We expect to filter out one registered gateway, but we find " + nrRegistered;
+
+        // Finally, create a license object
+        final LicenseObject l1 = createBasicLicenseObject("thelicense");
+
+        assert !sgo1.isRegistered() : "We just created a Staful GW object, is should not be registered";
+
+        // register sgo1 again and create an association in 1 go
+        License2GatewayAssociation lgw1 = runAndWaitForEvent(new Callable<License2GatewayAssociation>() {
+            public License2GatewayAssociation call() throws Exception {
+                sgo1.register();
+                return m_license2gatewayRepository.create(l1, sgo1.getGatewayObject());
+            }
+        }, false, License2GatewayAssociation.TOPIC_ADDED, GatewayObject.TOPIC_ADDED, StatefulGatewayObject.TOPIC_STATUS_CHANGED);
+
+        // checks
+        nrRegistered = m_statefulGatewayRepository.get(m_context.createFilter("(" + StatefulGatewayObject.KEY_REGISTRATION_STATE + "=" + RegistrationState.Registered + ")")).size();
+        assert nrRegistered == 2 : "We expect to filter out two registered gateways, but we find " + nrRegistered;
+        assert sgo1.isRegistered() : "A stateful gw object should be registered";
+        assert sgo1.isAssociated(l1, LicenseObject.class) : "The stateful gw object should be associated to thelicense.";
+        assert lgw1.isSatisfied() : "Both ends of license - stateful gw should be satisfied.";
+    }
+
+    private StatefulGatewayObject findStatefulGateway(String gatewayID) throws InvalidSyntaxException {
+        for (StatefulGatewayObject sgo : m_statefulGatewayRepository.get()) {
+            if (sgo.getID().equals(gatewayID)) {
+                return sgo;
+            }
+        }
+        return null;
+    }
+
+    @Test( groups = { TestUtils.INTEGRATION } )
+    public void testStatefulApprovalWithArtifacts() throws Exception {
+        // some setup: we need a helper.
+        ArtifactHelper myHelper = new MockArtifactHelper("mymime");
+
+        Properties serviceProps = new Properties();
+        serviceProps.put(ArtifactHelper.KEY_MIMETYPE, "mymime");
+
+        Service myHelperService = m_depManager.createService()
+            .setInterface(ArtifactHelper.class.getName(), serviceProps)
+            .setImplementation(myHelper);
+
+        m_depManager.add(myHelperService);
+
+        // Empty tag map to be reused througout test
+        final Map<String, String> tags = new HashMap<String, String>();
+
+        // First, create a bundle and an artifact, but do not provide a processor for the artifact.
+        ArtifactObject b1 = createBasicBundleObject("bundle1");
+        Map<String, String> attr = new HashMap<String, String>();
+        attr.put(ArtifactObject.KEY_URL, "http://myobject");
+        attr.put(ArtifactObject.KEY_PROCESSOR_PID, "my.processor.pid");
+        attr.put(ArtifactHelper.KEY_MIMETYPE, "mymime");
+
+        ArtifactObject a1 = m_artifactRepository.create(attr, tags);
+
+        GroupObject g = createBasicGroupObject("group");
+        LicenseObject l = createBasicLicenseObject("license");
+
+        attr = new HashMap<String, String>();
+        attr.put(GatewayObject.KEY_ID, "myGateway");
+
+        StatefulGatewayObject sgo = m_statefulGatewayRepository.preregister(attr, tags);
+
+        m_artifact2groupRepository.create(b1, g);
+        m_artifact2groupRepository.create(a1, g);
+
+        m_group2licenseRepository.create(g, l);
+
+        m_license2gatewayRepository.create(l, sgo.getGatewayObject());
+
+        try {
+            sgo.approve();
+            assert false : "Without a resource processor for our artifact, approve should go wrong.";
+        }
+        catch (IllegalStateException ise) {
+            // expected
+        }
+
+        // Now, add a processor for the artifact.
+        attr = new HashMap<String, String>();
+        attr.put(ArtifactObject.KEY_URL, "http://myprocessor");
+        attr.put(BundleHelper.KEY_RESOURCE_PROCESSOR_PID, "my.processor.pid");
+        attr.put(BundleHelper.KEY_SYMBOLICNAME, "my.processor.bundle");
+        attr.put(ArtifactHelper.KEY_MIMETYPE, BundleHelper.MIMETYPE);
+
+        ArtifactObject b2 = m_artifactRepository.create(attr, tags);
+
+        sgo.approve();
+
+        DeploymentVersionObject dep = m_deploymentVersionRepository.getMostRecentDeploymentVersion(sgo.getID());
+
+        DeploymentArtifact[] toDeploy = dep.getDeploymentArtifacts();
+
+        assert toDeploy.length == 3 : "We expect to find three artifacts to deploy, but we find: " + toDeploy.length;
+        DeploymentArtifact bundle1 = toDeploy[0];
+        assert bundle1.getUrl().equals(b1.getURL()) && (bundle1.getKeys().length == 2) : "The first artifact in the list should be bundle1, but it is '" + toDeploy[0].toString() + "'";
+        DeploymentArtifact bundle2 = toDeploy[1];
+        assert bundle2.getUrl().equals(b2.getURL()) && (bundle2.getKeys().length == 3) && bundle2.getDirective(DeploymentArtifact.DIRECTIVE_ISCUSTOMIZER).equals("true"): "The first artifact in the list should be bundle1, but it is '" + toDeploy[0].toString() + "'";
+        DeploymentArtifact artifact1 = toDeploy[2];
+        assert artifact1.getUrl().equals(a1.getURL()) && (artifact1.getKeys().length == 2) && artifact1.getDirective(DeploymentArtifact.DIRECTIVE_KEY_PROCESSORID).equals("my.processor.pid"): "The first artifact in the list should be bundle1, but it is '" + toDeploy[0].toString() + "'";
+
+        cleanUp();
+
+        m_depManager.remove(myHelperService);
+    }
+
+
+    @Test( groups = { TestUtils.INTEGRATION } )
+    public void testAutoGatewayOperator() throws Exception {
+        startRepositoryService();
+
+        addRepository("storeInstance", "luminis", "store", true);
+        addRepository("gatewayInstance", "luminis", "gateway", true);
+        addRepository("deploymentInstance", "luminis", "deployment", true);
+
+        // configure automation bundle; new configuration properties; bundle will start
+        final Properties props = new Properties();
+        props.put("registerGatewayFilter", "(id=anotherGate*)");
+        props.put("approveGatewayFilter", "(id=DO_NOTHING)");
+        props.put("autoApproveGatewayFilter", "(id=anotherGate*)");
+        props.put("commitRepositories","true");
+        props.put("gatewayRepository","gateway");
+        props.put("deploymentRepository","deployment");
+        props.put("storeRepository","store");
+        props.put("customerName","luminis");
+        props.put("hostName",HOST);
+        props.put("endpoint",ENDPOINT);
+
+        final Configuration config = m_configAdmin.getConfiguration("net.luminis.liq.client.automation", null);
+
+        /*
+         * First test the basic scenario where we create some auditlog data, this gateway should be auto-registered after max 1 sec.
+         */
+        runAndWaitForEvent(new Callable<Object>() {
+            public Object call() throws Exception {
+                config.update(props);
+                return null;
+            }
+        }, true, RepositoryAdmin.TOPIC_LOGIN);
+
+        testAutoGatewayReg();
+
+        runAndWaitForEvent(new Callable<Object>() {
+            public Object call() throws Exception {
+                config.delete();
+                return null;
+            }
+        }, false, RepositoryAdmin.TOPIC_LOGOUT);
+
+        // Remove all repositories
+        try {
+            removeAllRepositories();
+        }
+        catch (Exception e) {
+            // Not much we can do...
+        }
+
+        cleanUp();
+    }
+
+    private void testAutoGatewayReg() throws Exception {
+        List<LogEvent> events = new ArrayList<LogEvent>();
+        Properties props = new Properties();
+        events.add(new LogEvent("anotherGateway", 1, 1, 1, AuditEvent.FRAMEWORK_STARTED, props));
+        //fill auditlog; no install data
+        m_auditLogStore.put(events);
+
+        int initRepoSize = m_statefulGatewayRepository.get().size();
+
+        // Get the processauditlog task and run it
+        ServiceTracker tracker = new ServiceTracker(m_context, m_context.createFilter("(&(" + Constants.OBJECTCLASS + "=" + Runnable.class.getName() + ")(" + SchedulerConstants.SCHEDULER_NAME_KEY + "=" + "net.luminis.liq.client.processauditlog" + "))"), null);
+        tracker.open();
+
+        final Runnable processAuditlog = (Runnable) tracker.waitForService(2000);
+
+        if (processAuditlog != null) {
+            // commit should be called
+            runAndWaitForEvent(new Callable<Object>() {
+                public Object call() throws Exception {
+                    processAuditlog.run();
+                    return null;
+                }
+            }, false, RepositoryAdmin.TOPIC_REFRESH);
+
+            assert m_statefulGatewayRepository.get().size() == initRepoSize + 1 : "After refresh, we expect 1 gateway based on auditlogdata, but we find " + m_statefulGatewayRepository.get().size();
+            List<StatefulGatewayObject> sgoList = m_statefulGatewayRepository.get(m_context.createFilter("(id=anotherG*)"));
+            StatefulGatewayObject sgo = sgoList.get(0);
+            assert sgo != null : "Expected one (anotherGateway) in the list.";
+
+            // should be registered and auto approved
+            assert sgo.isRegistered(): "The automation gw operator should have registered anotherGateway.";
+            assert sgo.getAutoApprove(): "The automation gw operator should have auto-approved anotherGateway.";
+
+            // add a gateway which will not be autoregistered
+            events.clear();
+            events.add(new LogEvent("secondGateway", 1, 1, 1, AuditEvent.FRAMEWORK_STARTED, props));
+            m_auditLogStore.put(events);
+
+            // do auto gateway action
+            processAuditlog.run();
+            assert m_statefulGatewayRepository.get().size() == initRepoSize + 2 : "After refresh, we expect an additional gateway based on auditlogdata, but we find " + m_statefulGatewayRepository.get().size();
+            sgoList = m_statefulGatewayRepository.get(m_context.createFilter("(id=second*)"));
+            sgo = sgoList.get(0);
+
+            // second gateway should not be registered
+            assert !sgo.isRegistered(): "The automation gw operator should not have registered secongGateway.";
+            assert !sgo.getAutoApprove(): "The automation gw operator should not have auto-approved myGateway.";
+        }
+        else
+        {
+            assert false : "Could not get a reference to the processAuditLog task.";
+        }
+    }
+
+    @Test( groups = { TestUtils.INTEGRATION } )
+    public void testImportArtifactInvalidURL() throws Exception {
+        try {
+            m_artifactRepository.importArtifact(null, true);
+            assert false : "A null URL cannot be imported into an artifact repository.";
+        }
+        catch (IllegalArgumentException iae) {
+            // expected
+        }
+
+        URL invalidfile = new URL("file:/thisfilecannotexist");
+
+        try {
+            m_artifactRepository.importArtifact(invalidfile, true);
+            assert false : "An illegal URL should result in an error.";
+        }
+        catch (IllegalArgumentException iae) {
+            // expected
+        }
+
+        cleanUp();
+    }
+
+    @Test( groups = { TestUtils.INTEGRATION } )
+    public void testImportArtifactGeneralBundle() throws Exception {
+        // Use a valid JAR file, without a Bundle-SymbolicName header.
+        Manifest manifest = new Manifest();
+        Attributes attributes = manifest.getMainAttributes();
+        attributes.putValue(Attributes.Name.MANIFEST_VERSION.toString(), "1");
+        attributes.putValue(Constants.BUNDLE_MANIFESTVERSION, "2");
+
+        File temp = File.createTempFile("net.luminis.test", ".jar");
+        temp.deleteOnExit();
+        JarOutputStream jos = new JarOutputStream(new FileOutputStream(temp), manifest);
+        jos.close();
+
+        try {
+            m_artifactRepository.importArtifact(temp.toURI().toURL(), true);
+            assert false : "Without a Bundle-SymbolicName header, the BundleHelper cannot recognize this bundle.";
+        }
+        catch (IllegalArgumentException re) {
+            //expected
+        }
+
+        try {
+            m_artifactRepository.importArtifact(temp.toURI().toURL(), "notTheBundleMime", true);
+            assert false : "We have given an illegal mimetype, so no recognizer or helper can be found.";
+        }
+        catch (IllegalArgumentException re) {
+            //expected
+        }
+
+        // Use a valid JAR file, with a Bundle-SymbolicName header, but do not supply an OBR.
+        attributes.putValue(BundleHelper.KEY_SYMBOLICNAME, "net.luminis.test");
+
+        temp = File.createTempFile("net.luminis.test", ".jar");
+        temp.deleteOnExit();
+        jos = new JarOutputStream(new FileOutputStream(temp), manifest);
+        jos.close();
+
+        try {
+            m_artifactRepository.importArtifact(temp.toURI().toURL(), true);
+            assert false : "No OBR has been started, so the artifact repository should complain that there is no storage available.";
+        }
+        catch (IOException ise) {
+            //expected
+        }
+
+        // Supply the OBR.
+        addObr("/obr", "store");
+        m_artifactRepository.setObrBase(new URL("http://localhost:8080/obr/"));
+
+        m_artifactRepository.importArtifact(temp.toURI().toURL(), true);
+
+        assert m_artifactRepository.get().size() == 1;
+        assert m_artifactRepository.getResourceProcessors().size() == 0;
+
+        // Create a JAR file which looks like a resource processor supplying bundle.
+        attributes.putValue(BundleHelper.KEY_RESOURCE_PROCESSOR_PID, "someProcessor");
+
+        temp = File.createTempFile("net.luminis.test", ".jar");
+        temp.deleteOnExit();
+        jos = new JarOutputStream(new FileOutputStream(temp), manifest);
+        jos.close();
+
+        m_artifactRepository.importArtifact(temp.toURI().toURL(), true);
+
+        assert m_artifactRepository.get().size() == 1;
+        assert m_artifactRepository.getResourceProcessors().size() == 1;
+
+        deleteObr("/obr");
+    }
+
+    private class MockUser implements User {
+
+        private final String m_name;
+
+        public MockUser(String name) {
+            m_name = name;
+        }
+
+        @SuppressWarnings("unchecked")
+        public Dictionary getCredentials() {
+            return null;
+        }
+
+        public boolean hasCredential(String arg0, Object arg1) {
+            return false;
+        }
+
+        public String getName() {
+            return m_name;
+        }
+
+        @SuppressWarnings("unchecked")
+        public Dictionary getProperties() {
+            return null;
+        }
+
+        public int getType() {
+            return 0;
+        }
+    }
+
+    @Test( groups = { TestUtils.INTEGRATION } )
+    public void testLoginLogoutAndLoginOnceAgainWhileCreatingAnAssociation() throws IOException, InterruptedException, InvalidSyntaxException {
+        User user1 = new MockUser("user1");
+
+        startRepositoryService();
+
+        addRepository("storeInstance", "luminis", "store", true);
+        addRepository("gatewayInstance", "luminis", "gateway", true);
+
+        final RepositoryAdminLoginContext loginContext1 = m_repositoryAdmin.createLoginContext(user1);
+        loginContext1.addShopRepository(new URL(HOST + ENDPOINT), "luminis", "store", true);
+        loginContext1.addGatewayRepository(new URL(HOST + ENDPOINT), "luminis", "gateway", true);
+        m_repositoryAdmin.login(loginContext1);
+
+        GroupObject g1 = createBasicGroupObject("group1");
+        LicenseObject l1 = createBasicLicenseObject("license1");
+
+        m_group2licenseRepository.create(g1, l1);
+
+        m_repositoryAdmin.logout(false);
+
+        m_repositoryAdmin.login(loginContext1);
+
+        try {
+            removeAllRepositories();
+        }
+        catch (IOException ioe) {
+            // too bad.
+        }
+    }
+
+    /**
+     * Tests read only repository access: marking a repository as readonly for a login should
+     * mean that it does not get committed, but local changes will stay around between logins.
+     */
+    @Test( groups = { TestUtils.INTEGRATION } )
+    public void testReadOnlyRepositoryAccess() throws Exception {
+        User user1 = new MockUser("user1");
+
+        startRepositoryService();
+
+        addRepository("storeInstance", "luminis", "store", true);
+        addRepository("gatewayInstance", "luminis", "gateway", true);
+
+        final RepositoryAdminLoginContext loginContext1 = m_repositoryAdmin.createLoginContext(user1);
+        loginContext1.addShopRepository(new URL(HOST + ENDPOINT), "luminis", "store", true);
+        loginContext1.addGatewayRepository(new URL(HOST + ENDPOINT), "luminis", "gateway", false);
+        m_repositoryAdmin.login(loginContext1);
+
+        m_repositoryAdmin.checkout();
+
+        createBasicGroupObject("group1");
+        createBasicGatewayObject("gateway1");
+
+        m_repositoryAdmin.logout(false);
+
+        m_repositoryAdmin.login(loginContext1);
+
+        assert m_groupRepository.get().size() == 1 : "We expect our own group object in the repository; we find " + m_groupRepository.get().size();
+        assert m_gatewayRepository.get().size() == 1 : "We expect our own gateway object in the repository; we find " + m_gatewayRepository.get().size();
+
+        m_repositoryAdmin.commit();
+
+        m_repositoryAdmin.logout(false);
+
+        m_repositoryAdmin.login(loginContext1);
+
+        m_repositoryAdmin.checkout();
+
+        assert m_groupRepository.get().size() == 1 : "We expect our own group object in the repository; we find " + m_groupRepository.get().size();
+        assert m_gatewayRepository.get().size() == 0 : "Since the gateway repository will not be committed, we expect no gateway objects in the repository; we find " + m_gatewayRepository.get().size();
+
+        cleanUp();
+        try {
+            removeAllRepositories();
+        }
+        catch (IOException ioe) {
+            // too bad.
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    @Test( groups = { TestUtils.INTEGRATION } )
+    public void testRepostoryLoginDoubleRepository() throws Exception {
+        RepositoryAdminLoginContext context = m_repositoryAdmin.createLoginContext(new MockUser("user"));
+        context.addRepositories(new URL("http://localhost:8080"), "luminis", "shop", true, ArtifactRepository.class, Artifact2GroupAssociationRepository.class, GroupRepository.class);
+        context.addRepositories(new URL("http://localhost:8080"), "luminis", "deployment", true, GroupRepository.class, Group2LicenseAssociationRepository.class, LicenseRepository.class);
+        try {
+            m_repositoryAdmin.login(context);
+            assert false : "We tried to log in with two repositories that try to access the same repository service; this should not be allowed.";
+        }
+        catch (IllegalArgumentException iae) {
+            // expected
+        }
+    }
+
+    private static interface newRepository extends ObjectRepository<LicenseObject> {}
+
+    @SuppressWarnings("unchecked")
+    @Test( groups = { TestUtils.INTEGRATION } )
+    public void testRepostoryLoginRepositoryWithoutImplementation() throws Exception {
+        RepositoryAdminLoginContext context = m_repositoryAdmin.createLoginContext(new MockUser("user"));
+        context.addRepositories(new URL("http://localhost:8080"), "luminis", "shop", true, ArtifactRepository.class, Artifact2GroupAssociationRepository.class, GroupRepository.class);
+        context.addRepositories(new URL("http://localhost:8080"), "luminis", "deployment", true, GroupRepository.class, Group2LicenseAssociationRepository.class, newRepository.class);
+        try {
+            m_repositoryAdmin.login(context);
+            assert false : "We tried to log in with a repository for which no implementation is available; this should not be allowed.";
+        }
+        catch (IllegalArgumentException iae) {
+            // expected
+        }
+    }
+
+    /**
+     * Tests the template processing mechanism: given a custom processor, do the correct calls go out?
+     */
+    @SuppressWarnings("unchecked")
+    @Test( groups = { TestUtils.INTEGRATION } )
+    public void testTemplateProcessingInfrastructure() throws Exception {
+        // create a preprocessor
+        MockArtifactPreprocessor preprocessor = new MockArtifactPreprocessor();
+
+        // create a helper
+        MockArtifactHelper helper = new MockArtifactHelper("mymime", preprocessor);
+
+        // register preprocessor and helper
+        Properties serviceProps = new Properties();
+        serviceProps.put(ArtifactHelper.KEY_MIMETYPE, "mymime");
+
+        Service helperService = m_depManager.createService()
+            .setInterface(ArtifactHelper.class.getName(), serviceProps)
+            .setImplementation(helper);
+
+        m_depManager.add(helperService);
+
+        // create some tree from artifacts to a gateway
+        runAndWaitForEvent(new Callable<Object>() {
+            public Object call() throws Exception {
+                ArtifactObject b1 = createBasicBundleObject("myBundle");
+                ArtifactObject b2 = createBasicBundleObject("myProcessor", "1.0.0", "myProcessor.pid");
+                ArtifactObject a1 = createBasicArtifactObject("myArtifact", "mymime", "myProcessor.pid");
+                GroupObject go = createBasicGroupObject("mygroup");
+                LicenseObject lo = createBasicLicenseObject("mylicense");
+                GatewayObject gwo = createBasicGatewayObject("templategateway");
+                m_artifact2groupRepository.create(b1, go);
+                // note that we do not associate b2: this is a resource processor, so it will be packed
+                // implicitly. It should not be available to a preprocessor either.
+                m_artifact2groupRepository.create(a1, go);
+                m_group2licenseRepository.create(go, lo);
+                m_license2gatewayRepository.create(lo, gwo);
+                return null;
+            }
+        }, false, StatefulGatewayObject.TOPIC_ADDED);
+
+        StatefulGatewayObject sgo = m_statefulGatewayRepository.get(m_context.createFilter("(" + GatewayObject.KEY_ID + "=templategateway)")).get(0);
+
+        // wait until needsApprove is true; depending on timing, this could have happened before or after the TOPIC_ADDED.
+        int attempts = 0;
+        while (!sgo.needsApprove() && (attempts < 10)) {
+            Thread.sleep(10);
+        }
+        assert sgo.needsApprove() : "With the new assignments, the SGO should need approval.";
+        // create a deploymentversion
+        sgo.approve();
+
+//        // the preprocessor now has gotten its properties; inspect these
+        PropertyResolver gateway = preprocessor.getProps();
+        assert gateway.get("id").equals("templategateway") : "The property resolver should be able to resolve 'id'.";
+        assert gateway.get("name").equals("mylicense") : "The property resolver should be able to resolve 'name'.";
+        assert gateway.get("someunknownproperty") == null : "The property resolver should not be able to resolve 'someunknownproperty'.";
+
+        cleanUp(); // we need to do this before the helper goes away
+
+        m_depManager.remove(helperService);
+    }
+
+    /**
+     * Tests the full template mechanism, from importing templatable artifacts, to creating deployment
+     * versions with it. It uses the configuration (autoconf) helper, which uses a VelocityBased preprocessor.
+     */
+    @SuppressWarnings("unchecked")
+    @Test( groups = { TestUtils.INTEGRATION } )
+    public void testTemplateProcessing() throws Exception {
+        addObr("/obr", "store");
+        m_artifactRepository.setObrBase(new URL("http://localhost:8080/obr/"));
+
+        // create some template things
+        String xmlHeader = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<metatype:MetaData xmlns:metatype= \"http://www.osgi.org/xmlns/metatype/v1.0.0\">\n";
+        String xmlFooter = "\n</metatype:MetaData>";
+
+        String noTemplate = "<Attribute content=\"http://someURL\"/>";
+        String noTemplateProcessed = "<Attribute content=\"http://someURL\"/>";
+        final File noTemplateFile = createFileWithContents("template", "xml", xmlHeader+noTemplate+xmlFooter);
+
+        String simpleTemplate = "<Attribute content=\"http://$context.name\"/>";
+        String simpleTemplateProcessed = "<Attribute content=\"http://mylicense\"/>";
+        File simpleTemplateFile = createFileWithContents("template", "xml", xmlHeader+simpleTemplate+xmlFooter);
+
+        // create some tree from artifacts to a gateway
+        GroupObject go = runAndWaitForEvent(new Callable<GroupObject>() {
+            public GroupObject call() throws Exception {
+                ArtifactObject b1 = createBasicBundleObject("myBundle");
+                ArtifactObject b2 = createBasicBundleObject("myProcessor", "1.0.0", "org.osgi.deployment.rp.autoconf");
+                GroupObject go = createBasicGroupObject("mygroup");
+                LicenseObject lo = createBasicLicenseObject("mylicense");
+                GatewayObject gwo = createBasicGatewayObject("templategateway2");
+                m_artifact2groupRepository.create(b1, go);
+                // note that we do not associate b2: this is a resource processor, so it will be packed
+                // implicitly. It should not be available to a preprocessor either.
+                m_group2licenseRepository.create(go, lo);
+                m_license2gatewayRepository.create(lo, gwo);
+                return go;
+            }
+        }, false, StatefulGatewayObject.TOPIC_ADDED);
+
+        ArtifactObject a1 = m_artifactRepository.importArtifact(noTemplateFile.toURI().toURL(), true);
+        Artifact2GroupAssociation a2g = m_artifact2groupRepository.create(a1, go);
+
+        final StatefulGatewayObject sgo = m_statefulGatewayRepository.get(m_context.createFilter("(" + GatewayObject.KEY_ID + "=templategateway2)")).get(0);
+
+        // create a deploymentversion
+        assert sgo.needsApprove() : "With the new assignments, the SGO should need approval.";
+        runAndWaitForEvent(new Callable<Object>() {
+            public Object call() throws Exception {
+                sgo.approve();
+                return null;
+            }
+        }, false, StatefulGatewayObject.TOPIC_STATUS_CHANGED);
+
+        // find the deployment version
+        DeploymentVersionObject dvo = m_deploymentVersionRepository.getMostRecentDeploymentVersion("templategateway2");
+        String inFile = tryGetStringFromURL(findXmlUrlInDeploymentObject(dvo), 10, 100);
+
+        assert inFile.equals(xmlHeader + noTemplateProcessed + xmlFooter) : "We expected to find\n" + xmlHeader + noTemplateProcessed + xmlFooter + "\n in the processed artifact, but found\n" + inFile;
+
+        // try the simple template
+        m_artifact2groupRepository.remove(a2g);
+        a1 = m_artifactRepository.importArtifact(simpleTemplateFile.toURI().toURL(), true);
+        a2g = m_artifact2groupRepository.create(a1, go);
+
+        sgo.approve();
+
+        // find the deployment version
+        dvo = m_deploymentVersionRepository.getMostRecentDeploymentVersion("templategateway2");
+        // sleep for a while, to allow the OBR to process the file.
+        Thread.sleep(1000);
+
+        inFile = tryGetStringFromURL(findXmlUrlInDeploymentObject(dvo), 10, 100);
+
+        assert inFile.equals(xmlHeader + simpleTemplateProcessed + xmlFooter) : "We expected to find\n" + xmlHeader + simpleTemplateProcessed + xmlFooter + "\n in the processed artifact, but found\n" + inFile;
+
+        deleteObr("/obr");
+    }
+
+    private String tryGetStringFromURL(URL url, int tries, int interval) throws Exception {
+        while (true) {
+            try {
+                String result = getStringFromURL(url);
+                return result;
+            }
+            catch (IOException ioe) {
+                Thread.sleep(interval);
+                tries--;
+                if (tries == 0) {
+                   throw ioe;
+                }
+            }
+        }
+
+    }
+
+    /**
+     * Helper method for testTemplateProcessing; finds the URL of the first deploymentartifact
+     * with 'xml' in its url.
+     */
+    private URL findXmlUrlInDeploymentObject(DeploymentVersionObject dvo) throws MalformedURLException {
+        DeploymentArtifact[] artifacts = dvo.getDeploymentArtifacts();
+        for (DeploymentArtifact da : artifacts) {
+            if (da.getUrl().contains("xml")) {
+                return new URL(da.getUrl());
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Creates a temporary file with the given name and extension, and stores the given
+     * contents in it.
+     */
+    private File createFileWithContents(String name, String extension, String contents) throws IOException {
+        File file = File.createTempFile(name, extension);
+        file.deleteOnExit();
+        Writer w = new OutputStreamWriter(new FileOutputStream(file));
+        w.write(contents);
+        w.close();
+        return file;
+    }
+
+    /**
+     * Opens a URL, and gets all data from it as a string.
+     */
+    private String getStringFromURL(URL url) throws IOException {
+        StringBuilder found = new StringBuilder();
+        Reader reader = new InputStreamReader(url.openStream());
+
+        char[] buf = new char[1024];
+        for (int count = reader.read(buf); count != -1; count = reader.read(buf)) {
+            found.append(buf, 0, count);
+        }
+        reader.close();
+        return found.toString();
+    }
+
+    private volatile List<String> m_waitingForTopic = Collections.synchronizedList(new ArrayList<String>());
+    private volatile Semaphore m_semaphore;
+    private volatile boolean m_runAndWaitDebug = false;
+
+    private <T> T runAndWaitForEvent(Callable<T> callable, boolean debug, String... topic) throws Exception {
+        m_runAndWaitDebug = debug;
+        T result = null;
+        m_waitingForTopic.clear();
+        m_waitingForTopic.addAll(Arrays.asList(topic));
+        m_semaphore = new Semaphore(0);
+        result = callable.call();
+        assert m_semaphore.tryAcquire(5000, TimeUnit.MILLISECONDS) : "We expect the event within a reasonable timeout.";
+        m_semaphore = null;
+        return result;
+    }
+
+    public void handleEvent(Event event) {
+        if (m_runAndWaitDebug) {System.err.println("Received event: " + event.getTopic());}
+        if (m_waitingForTopic.remove(event.getTopic())) {
+            if (m_runAndWaitDebug) {System.err.println("Event was expected.");}
+            if ((m_semaphore != null) && m_waitingForTopic.isEmpty()) {
+                m_semaphore.release();
+                m_runAndWaitDebug = false;
+            }
+        }
+    }
+
+    private ArtifactObject createBasicBundleObject(String symbolicName) {
+        return createBasicBundleObject(symbolicName, null, null);
+    }
+
+    private ArtifactObject createBasicBundleObject(String symbolicName, String version, String processorPID) {
+        Map<String, String> attr = new HashMap<String, String>();
+        attr.put(BundleHelper.KEY_SYMBOLICNAME, symbolicName);
+        attr.put(ArtifactObject.KEY_MIMETYPE, BundleHelper.MIMETYPE);
+        attr.put(ArtifactObject.KEY_URL, "http://" + symbolicName + "-" + ((version == null) ? "null" : version));
+        Map<String, String> tags = new HashMap<String, String>();
+
+        if (version != null) {
+            attr.put(BundleHelper.KEY_VERSION, version);
+        }
+        if (processorPID != null) {
+            attr.put(BundleHelper.KEY_RESOURCE_PROCESSOR_PID, processorPID);
+        }
+        return m_artifactRepository.create(attr, tags);
+    }
+
+    private ArtifactObject createBasicArtifactObject(String name, String mimetype, String processorPID) throws InterruptedException {
+        Map<String, String> attr = new HashMap<String, String>();
+        attr.put(ArtifactObject.KEY_ARTIFACT_NAME, name);
+        attr.put(ArtifactObject.KEY_MIMETYPE, mimetype);
+        attr.put(ArtifactObject.KEY_URL, "http://" + name);
+        attr.put(ArtifactObject.KEY_PROCESSOR_PID, processorPID);
+        Map<String, String> tags = new HashMap<String, String>();
+
+        return m_artifactRepository.create(attr, tags);
+    }
+
+    private GroupObject createBasicGroupObject(String name) {
+        Map<String, String> attr = new HashMap<String, String>();
+        attr.put(GroupObject.KEY_NAME, name);
+        Map<String, String> tags = new HashMap<String, String>();
+
+        return m_groupRepository.create(attr, tags);
+    }
+
+    private LicenseObject createBasicLicenseObject(String name) {

[... 237 lines stripped ...]