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 [12/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/server/src/net/luminis/liq/client/repository/stateful/impl/StatefulGatewayObjectImpl.java
URL: http://svn.apache.org/viewvc/incubator/ace/trunk/server/src/net/luminis/liq/client/repository/stateful/impl/StatefulGatewayObjectImpl.java?rev=788992&view=auto
==============================================================================
--- incubator/ace/trunk/server/src/net/luminis/liq/client/repository/stateful/impl/StatefulGatewayObjectImpl.java (added)
+++ incubator/ace/trunk/server/src/net/luminis/liq/client/repository/stateful/impl/StatefulGatewayObjectImpl.java Sat Jun 27 15:53:04 2009
@@ -0,0 +1,699 @@
+package net.luminis.liq.client.repository.stateful.impl;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.Dictionary;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.Properties;
+
+import net.luminis.liq.client.repository.Associatable;
+import net.luminis.liq.client.repository.Association;
+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.License2GatewayAssociation;
+import net.luminis.liq.client.repository.object.LicenseObject;
+import net.luminis.liq.client.repository.stateful.StatefulGatewayObject;
+import net.luminis.liq.log.AuditEvent;
+import net.luminis.liq.log.LogDescriptor;
+import net.luminis.liq.log.LogEvent;
+
+/**
+ * A <code>StatefulGatewayObjectImpl</code> uses the interface of a <code>StatefulGatewayObject</code>,
+ * but delegates most of its calls to either an embedded <code>GatewayObject</code>, or to its
+ * parent <code>StatefulGatewayRepository</code>. Once created, it will handle its own lifecyle
+ * and remove itself once is existence is no longer necessary.
+ */
+public class StatefulGatewayObjectImpl implements StatefulGatewayObject {
+    private final StatefulGatewayRepositoryImpl m_repository;
+    private final Object m_lock = new Object();
+    private GatewayObject m_gatewayObject;
+    private List<LogDescriptor> m_processedAuditEvents = new ArrayList<LogDescriptor>();
+    private Map<String, String> m_attributes = new HashMap<String, String>();
+    /** This boolean is used to suppress STATUS_CHANGED events during the creation of the object.*/
+    private boolean m_inConstructor = true;
+
+    /**
+     * Creates a new <code>StatefulGatewayObjectImpl</code>. After creation, it will have the
+     * most recent data available, and has verified its own reasons for existence.
+     * @param repository The parent repository of this object.
+     * @param gatewayID A string representing a gateway ID.
+     */
+    StatefulGatewayObjectImpl(StatefulGatewayRepositoryImpl repository, String gatewayID) {
+        m_repository = repository;
+        addStatusAttribute(KEY_ID, gatewayID);
+        updateGatewayObject(false);
+        updateAuditEvents(false);
+        updateDeploymentVersions(null);
+        verifyExistence();
+        m_inConstructor = false;
+    }
+
+    public String approve() throws IllegalStateException {
+        try {
+            String version = m_repository.approve(getID());
+            setStoreState(StoreState.Approved);
+            return version;
+        }
+        catch (IOException e) {
+            throw new IllegalStateException("Problem generating new deployment version: " + e);
+        }
+    }
+
+    public List<LogEvent> getAuditEvents() {
+        return m_repository.getAuditEvents(getID());
+    }
+
+    public String getCurrentVersion() {
+        DeploymentVersionObject version = m_repository.getMostRecentDeploymentVersion(getID());
+        if (version == null) {
+            return StatefulGatewayObject.UNKNOWN_VERSION;
+        }
+        else {
+            return version.getVersion();
+        }
+    }
+
+    public void register() throws IllegalStateException {
+        m_repository.register(getID());
+    }
+
+    public boolean isRegistered() {
+        synchronized(m_lock) {
+            return (m_gatewayObject != null);
+        }
+    }
+
+    public GatewayObject getGatewayObject() {
+        synchronized(m_lock) {
+            ensureGatewayPresent();
+            return m_gatewayObject;
+        }
+    }
+
+    public DeploymentArtifact[] getArtifactsFromDeployment() {
+        synchronized(m_lock) {
+            DeploymentVersionObject mostRecentDeploymentVersion = m_repository.getMostRecentDeploymentVersion(getID());
+            if (mostRecentDeploymentVersion != null) {
+                return mostRecentDeploymentVersion.getDeploymentArtifacts();
+            }
+            return new DeploymentArtifact[0];
+        }
+    }
+
+    public ArtifactObject[] getArtifactsFromShop() {
+        return m_repository.getNecessaryArtifacts(getID());
+    }
+
+    public boolean getLastInstallSuccess() {
+        synchronized(m_lock) {
+            return Boolean.parseBoolean(getStatusAttribute(KEY_LAST_INSTALL_SUCCESS));
+        }
+    }
+
+    public String getLastInstallVersion() {
+        synchronized(m_lock) {
+            return getStatusAttribute(KEY_LAST_INSTALL_VERSION);
+        }
+    }
+
+    public void acknowledgeInstallVersion(String version) {
+        synchronized(m_lock) {
+            addStatusAttribute(KEY_ACKNOWLEDGED_INSTALL_VERSION, version);
+            if (version.equals(getStatusAttribute(KEY_LAST_INSTALL_VERSION))) {
+                setProvisioningState(ProvisioningState.Idle);
+            }
+        }
+    }
+
+    public boolean needsApprove() {
+        return getStoreState() == StoreState.Unapproved;
+    }
+
+    public ProvisioningState getProvisioningState() {
+        return ProvisioningState.valueOf(getStatusAttribute(KEY_PROVISIONING_STATE));
+    }
+
+    public RegistrationState getRegistrationState() {
+        return RegistrationState.valueOf(getStatusAttribute(KEY_REGISTRATION_STATE));
+    }
+
+    public StoreState getStoreState() {
+        String statusAttribute = getStatusAttribute(KEY_STORE_STATE);
+        if (statusAttribute != null) {
+            return StoreState.valueOf(statusAttribute);
+        }
+        return StoreState.New;
+    }
+
+    /**
+     * Signals this object that there has been a change to the <code>GatewayObject</code> it represents.
+     * @param needsVerify States whether this update should make the object check for its
+     * reasons for existence.
+     */
+    void updateGatewayObject(boolean needsVerify) {
+        synchronized(m_lock) {
+            m_gatewayObject = m_repository.getGatewayObject(getID());
+            determineRegistrationState();
+            if (needsVerify) {
+                verifyExistence();
+            }
+        }
+    }
+
+    /**
+     * Signals this object that there has been a change to the auditlog which may interest
+     * this object.
+     * @param needsVerify States whether this update should make the object check for its
+     * reasons for existence.
+     */
+    void updateAuditEvents(boolean needsVerify) {
+        synchronized(m_lock) {
+            determineProvisioningState();
+            if (needsVerify) {
+                verifyExistence();
+            }
+        }
+    }
+
+    /**
+     * Signals this object that a new deployment version has been created in relation
+     * to the gatewayID this object manages.
+     */
+    void updateDeploymentVersions(DeploymentVersionObject deploymentVersionObject) {
+        synchronized(m_lock) {
+            determineProvisioningState();
+            determineStoreState(deploymentVersionObject);
+        }
+    }
+
+    /**
+     * Based on the information about a <code>GatewayObject</code>, the
+     * <code>AuditEvent</code>s available, and the deployment information that
+     * the parent repository can give, determines the status of this gateway.
+     */
+    void determineStatus() {
+        determineRegistrationState();
+        determineProvisioningState();
+        determineStoreState(null);
+        verifyExistence();
+    }
+
+    private void determineRegistrationState() {
+        synchronized(m_lock) {
+            if (!isRegistered()) {
+                setRegistrationState(RegistrationState.Unregistered);
+            }
+            else {
+                setRegistrationState(RegistrationState.Registered);
+            }
+        }
+    }
+
+    private void determineStoreState(DeploymentVersionObject deploymentVersionObject) {
+        synchronized(m_lock) {
+            List<String> fromShop = new ArrayList<String>();
+            ArtifactObject[] artifactsFromShop = m_repository.getNecessaryArtifacts(getID());
+            DeploymentVersionObject mostRecentVersion;
+            if (deploymentVersionObject == null) {
+                mostRecentVersion = m_repository.getMostRecentDeploymentVersion(getID());
+            }
+            else {
+                mostRecentVersion = deploymentVersionObject;
+            }
+            if (artifactsFromShop == null) {
+                if (mostRecentVersion == null) {
+                    setStoreState(StoreState.New);
+                }
+                else {
+                    setStoreState(StoreState.Unapproved);
+                }
+                return;
+            }
+
+            for (ArtifactObject ao : artifactsFromShop) {
+                fromShop.add(ao.getURL());
+            }
+
+            List<String> fromDeployment = new ArrayList<String>();
+            for (DeploymentArtifact da : getArtifactsFromDeployment()) {
+                fromDeployment.add(da.getDirective(DeploymentArtifact.DIRECTIVE_KEY_BASEURL));
+            }
+
+            if ((mostRecentVersion == null) && fromShop.isEmpty()) {
+                setStoreState(StoreState.New);
+            }
+            else if (fromShop.containsAll(fromDeployment) && fromDeployment.containsAll(fromShop)) {
+                // great, we have the same artifacts. But... do they need to be reprocessed?
+                for (ArtifactObject ao : artifactsFromShop) {
+                    if (m_repository.needsNewVersion(ao, getID(), mostRecentVersion.getVersion())) {
+                        setStoreState(StoreState.Unapproved);
+                        return;
+                    }
+                }
+                setStoreState(StoreState.Approved);
+            }
+            else {
+                setStoreState(StoreState.Unapproved);
+            }
+        }
+    }
+
+    private void determineProvisioningState() {
+        /*
+         * This method gets all audit events it has not yet seen, and goes through them, backward
+         * in time, to find either and INSTALL or a COMPLETE event. A INSTALL event gives us a version,
+         * and tells us we're in InProgress. A COMPLETE tells gives us a version, and a success. The success
+         * will be stored, and also sets the state to OK or Failed, unless the version we found has already been
+         * acknowledged, the the state is set to Idle. Also, if there is no information whatsoever, we assume Idle.
+         */
+        synchronized(m_lock) {
+            List<LogDescriptor> allDescriptors = m_repository.getAllDescriptors(getID());
+            List<LogDescriptor> newDescriptors = m_repository.diffLogDescriptorLists(allDescriptors, m_processedAuditEvents);
+
+            List<LogEvent> newEvents = m_repository.getAuditEvents(newDescriptors);
+            for (int position = newEvents.size() - 1; position >= 0; position--) {
+                String currentVersion = (String) newEvents.get(position).getProperties().get(AuditEvent.KEY_VERSION);
+                if (newEvents.get(position).getType() == AuditEvent.DEPLOYMENTCONTROL_INSTALL) {
+                    addStatusAttribute(KEY_LAST_INSTALL_VERSION, currentVersion);
+                    setProvisioningState(ProvisioningState.InProgress);
+                    sendNewAuditlog(newDescriptors);
+                    m_processedAuditEvents = allDescriptors;
+                    return;
+                }
+                else if (newEvents.get(position).getType() == AuditEvent.DEPLOYMENTADMIN_COMPLETE) {
+                    addStatusAttribute(KEY_LAST_INSTALL_VERSION, currentVersion);
+                    if ((currentVersion != null) && currentVersion.equals(getStatusAttribute(KEY_ACKNOWLEDGED_INSTALL_VERSION))) {
+                        setProvisioningState(ProvisioningState.Idle);
+                        sendNewAuditlog(newDescriptors);
+                        m_processedAuditEvents = allDescriptors;
+                        return;
+                    }
+                    else {
+                        String value = (String) newEvents.get(position).getProperties().get(AuditEvent.KEY_SUCCESS);
+                        addStatusAttribute(KEY_LAST_INSTALL_SUCCESS, value);
+                        if (Boolean.parseBoolean(value)) {
+                            setProvisioningState(ProvisioningState.OK);
+                            sendNewAuditlog(newDescriptors);
+                            m_processedAuditEvents = allDescriptors;
+                            return;
+                        }
+                        else {
+                            setProvisioningState(ProvisioningState.Failed);
+                            sendNewAuditlog(newDescriptors);
+                            m_processedAuditEvents = allDescriptors;
+                            return;
+                        }
+                    }
+                }
+            }
+
+            if (m_processedAuditEvents.isEmpty()) {
+                setProvisioningState(ProvisioningState.Idle);
+            }
+            sendNewAuditlog(newDescriptors);
+            m_processedAuditEvents = allDescriptors;
+        }
+    }
+
+    private void sendNewAuditlog(List<LogDescriptor> events) {
+        // Check whether there are actually events in the list.
+        boolean containsData = false;
+        for (LogDescriptor l : events) {
+            containsData |= (l.getRangeSet().getHigh() != 0);
+        }
+
+        if (containsData) {
+            Properties props = new Properties();
+            props.put(StatefulGatewayObject.KEY_AUDITEVENTS, events);
+            m_repository.notifyChanged(this, TOPIC_AUDITEVENTS_CHANGED, props);
+        }
+    }
+
+    private void setRegistrationState(RegistrationState state) {
+        setStatus(KEY_REGISTRATION_STATE, state.toString());
+    }
+
+    private void setStoreState(StoreState state) {
+        setStatus(KEY_STORE_STATE, state.toString());
+    }
+
+    private void setProvisioningState(ProvisioningState state) {
+        setStatus(KEY_PROVISIONING_STATE, state.toString());
+    }
+
+    private void setStatus(String key, String status) {
+        if (!status.equals(getStatusAttribute(key))) {
+            addStatusAttribute(key, status);
+            handleStatechangeAutomation();
+            if (!m_inConstructor) {
+                m_repository.notifyChanged(this, TOPIC_STATUS_CHANGED);
+            }
+        }
+    }
+
+    private void handleStatechangeAutomation() {
+        if (getStoreState().equals(StoreState.Unapproved) && isRegistered() && getAutoApprove()) {
+            approve();
+        }
+    }
+
+    /**
+     * Verifies that this object should still be around. If the gateway is represents
+     * shows up in at least the gateway repository or the auditlog, it has a reason
+     * to exists; if not, it doesn't. When it is no longer necessary, it will remove itself
+     * from the parent repository.
+     * @return Whether or not this object should still exist.
+     */
+    boolean verifyExistence() {
+        synchronized(m_lock) {
+            if ((m_gatewayObject == null) && ((m_processedAuditEvents == null) || m_processedAuditEvents.isEmpty())) {
+                m_repository.removeStateful(this);
+                return false;
+            }
+            return true;
+        }
+    }
+
+    /**
+     * Helper method for the delegate methods below: most of these delegate their calls to a
+     * <code>GatewayObject</code>, but in order to do so, one must be present.
+     */
+    private void ensureGatewayPresent() {
+        if ((m_gatewayObject == null)) {
+            throw new IllegalStateException("This StatefulGatewayObject is not backed by a GatewayObject.");
+            // NOTE: we do not check the isDeleted state; the GatewayObject itself will notify the user of this.
+        }
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if ((o == null) || !(o instanceof StatefulGatewayObject)) {
+            return false;
+        }
+        return getID() == ((StatefulGatewayObject) o).getID();
+    }
+
+    private void addStatusAttribute(String key, String value) {
+        m_attributes.put(key, value);
+    }
+
+    private String getStatusAttribute(String key) {
+        return m_attributes.get(key);
+    }
+
+    /* ******************
+     * Delegates to GatewayObject
+     */
+
+    public String getID() {
+        return getStatusAttribute(KEY_ID);
+    }
+
+    public boolean isDeleted() {
+        return !verifyExistence();
+    }
+
+    public List<License2GatewayAssociation> getAssociationsWith(LicenseObject license) {
+        synchronized(m_lock) {
+            ensureGatewayPresent();
+            return m_gatewayObject.getAssociationsWith(license);
+        }
+    }
+
+    public List<LicenseObject> getLicenses() {
+        synchronized(m_lock) {
+            ensureGatewayPresent();
+            return m_gatewayObject.getLicenses();
+        }
+    }
+
+    public String addAttribute(String key, String value) {
+        synchronized(m_lock) {
+            ensureGatewayPresent();
+            return m_gatewayObject.addAttribute(key, value);
+        }
+    }
+
+    public String addTag(String key, String value) {
+        synchronized(m_lock) {
+            ensureGatewayPresent();
+            return m_gatewayObject.addTag(key, value);
+        }
+    }
+
+    public String getAttribute(String key) {
+        // retrieve from both
+        synchronized(m_lock) {
+            if (Arrays.binarySearch(KEYS_ALL, key) >= 0) {
+                return getStatusAttribute(key);
+            }
+            ensureGatewayPresent();
+            return m_gatewayObject.getAttribute(key);
+        }
+    }
+
+    public Enumeration<String> getAttributeKeys() {
+        synchronized(m_lock) {
+            List<String> statusKeys = new ArrayList<String>();
+            for (String s : KEYS_ALL) {
+                statusKeys.add(s);
+            }
+            Enumeration<String> attributeKeys = null;
+            if (m_gatewayObject != null) {
+                attributeKeys = m_gatewayObject.getAttributeKeys();
+            }
+            return new ExtendedEnumeration<String>(attributeKeys, statusKeys, true);
+        }
+    }
+
+    public Dictionary<String, Object> getDictionary() {
+        // build our own dictionary
+        synchronized(m_lock) {
+            return new StatefulGatewayObjectDictionary();
+        }
+    }
+
+    public String getTag(String key) {
+        synchronized(m_lock) {
+            ensureGatewayPresent();
+            return m_gatewayObject.getTag(key);
+        }
+    }
+
+    public Enumeration<String> getTagKeys() {
+        synchronized(m_lock) {
+            ensureGatewayPresent();
+            return m_gatewayObject.getTagKeys();
+        }
+    }
+
+    public boolean getAutoApprove() {
+        synchronized(m_lock) {
+            if (m_gatewayObject != null) {
+                return m_gatewayObject.getAutoApprove();
+            }
+            else {
+                return false;
+            }
+
+        }
+    }
+
+    public void setAutoApprove(boolean approve) {
+        synchronized(m_lock) {
+            ensureGatewayPresent();
+            m_gatewayObject.setAutoApprove(approve);
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    public <T extends Associatable> void add(Association association, Class<T> clazz) {
+        synchronized(m_lock) {
+            ensureGatewayPresent();
+            m_gatewayObject.add(association, clazz);
+        }
+    }
+
+    public <T extends Associatable> List<T> getAssociations(Class<T> clazz) {
+        synchronized(m_lock) {
+            ensureGatewayPresent();
+            return m_gatewayObject.getAssociations(clazz);
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    public <T extends Associatable, A extends Association> List<A> getAssociationsWith(Associatable other, Class<T> clazz, Class<A> associationType) {
+        synchronized(m_lock) {
+            ensureGatewayPresent();
+            return m_gatewayObject.getAssociationsWith(other, clazz, associationType);
+        }
+    }
+
+    public <T extends Associatable> boolean isAssociated(Object obj, Class<T> clazz) {
+        synchronized(m_lock) {
+            ensureGatewayPresent();
+            return m_gatewayObject.isAssociated(obj, clazz);
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    public <T extends Associatable> void remove(Association association, Class<T> clazz) {
+        synchronized(m_lock) {
+            ensureGatewayPresent();
+            m_gatewayObject.remove(association, clazz);
+        }
+    }
+
+    public String getDefinition() {
+        synchronized(m_lock) {
+            ensureGatewayPresent();
+            return m_gatewayObject.getDefinition();
+        }
+    }
+
+    private class ExtendedEnumeration<T> implements Enumeration<T> {
+        private Enumeration<T> m_source;
+        private List<T> m_extra;
+        private final boolean m_allowDuplicates;
+
+        ExtendedEnumeration(Enumeration<T> source, List<T> extra, boolean allowDuplicates) {
+            m_source = source;
+            m_extra = extra;
+            m_allowDuplicates = allowDuplicates;
+        }
+
+        public boolean hasMoreElements() {
+            boolean inSource = (m_source != null);
+            boolean inExtra = false;
+            if (m_extra != null) {
+                inExtra = !m_extra.isEmpty();
+            }
+            return inSource || inExtra;
+        }
+
+        public T nextElement() {
+            if (m_source != null) {
+                T result = m_source.nextElement();
+                if (!m_source.hasMoreElements()) {
+                    m_source = null;
+                }
+                if (!m_allowDuplicates) {
+                    m_extra.remove(result);
+                }
+                return result;
+            }
+            else if (!m_extra.isEmpty()) {
+                return m_extra.remove(0);
+            }
+            throw new NoSuchElementException();
+        }
+    }
+
+    private class StatefulGatewayObjectDictionary extends Dictionary<String, Object> {
+        private final Dictionary<String, Object> m_dict;
+
+        StatefulGatewayObjectDictionary() {
+            if (m_gatewayObject != null) {
+                m_dict = m_gatewayObject.getDictionary();
+            }
+            else {
+                m_dict = null;
+            }
+        }
+
+        @Override
+        public Enumeration<Object> elements() {
+            List<Object> statusVals = new ArrayList<Object>();
+            for (String key : KEYS_ALL) {
+                statusVals.add(getStatusAttribute(key));
+            }
+            Enumeration<Object> attributeVals = null;
+            if (m_dict != null) {
+                attributeVals = m_dict.elements();
+            }
+            return new ExtendedEnumeration<Object>(attributeVals, statusVals, true);
+        }
+
+        @Override
+        public Object get(Object key) {
+            for (String s : KEYS_ALL) {
+                if (s.equals(key)) {
+                    return getStatusAttribute((String) key);
+                }
+            }
+            String tag = m_gatewayObject.getTag((String)key);
+            String attr = m_gatewayObject.getAttribute((String)key);
+            if (tag == null) {
+                return attr;
+            }
+            else if (attr == null) {
+                return tag;
+            }
+            else {
+                return new String[] {attr, tag};
+            }
+        }
+
+        @Override
+        public boolean isEmpty() {
+            // This is always false, since we always have the status attributes.
+            return false;
+        }
+
+        @Override
+        public Enumeration<String> keys() {
+            List<String> statusKeys = new ArrayList<String>();
+            for (String key : KEYS_ALL) {
+                statusKeys.add(key);
+            }
+            Enumeration<String> attributeKeys = null;
+            if (m_dict != null) {
+                attributeKeys = m_dict.keys();
+            }
+            return new ExtendedEnumeration<String>(attributeKeys, statusKeys, false);
+        }
+
+        @Override
+        public Object put(String key, Object value) {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public Object remove(Object key) {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public int size() {
+            int result = 0;
+            Enumeration<String> keys = keys();
+            while (keys.hasMoreElements()) {
+                result++;
+                keys.nextElement();
+            }
+            return result;
+        }
+    }
+
+    public String getAssociationFilter(Map<String, String> properties) {
+        throw new UnsupportedOperationException("A StatefulGatewayObject cannot return a filter; use the underlying GatewayObject instead.");
+    }
+
+    public int getCardinality(Map<String, String> properties) {
+        throw new UnsupportedOperationException("A StatefulGatewayObject cannot return a cardinality; use the underlying GatewayObject instead.");
+    }
+
+    @SuppressWarnings("unchecked")
+    public Comparator getComparator() {
+        throw new UnsupportedOperationException("A StatefulGatewayObject cannot return a comparator; use the underlying GatewayObject instead.");
+    }
+
+}

Added: incubator/ace/trunk/server/src/net/luminis/liq/client/repository/stateful/impl/StatefulGatewayRepositoryImpl.java
URL: http://svn.apache.org/viewvc/incubator/ace/trunk/server/src/net/luminis/liq/client/repository/stateful/impl/StatefulGatewayRepositoryImpl.java?rev=788992&view=auto
==============================================================================
--- incubator/ace/trunk/server/src/net/luminis/liq/client/repository/stateful/impl/StatefulGatewayRepositoryImpl.java (added)
+++ incubator/ace/trunk/server/src/net/luminis/liq/client/repository/stateful/impl/StatefulGatewayRepositoryImpl.java Sat Jun 27 15:53:04 2009
@@ -0,0 +1,645 @@
+package net.luminis.liq.client.repository.stateful.impl;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+import net.luminis.liq.client.repository.RepositoryAdmin;
+import net.luminis.liq.client.repository.RepositoryObject;
+import net.luminis.liq.client.repository.RepositoryUtil;
+import net.luminis.liq.client.repository.helper.bundle.BundleHelper;
+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.GroupObject;
+import net.luminis.liq.client.repository.object.LicenseObject;
+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.stateful.StatefulGatewayObject;
+import net.luminis.liq.client.repository.stateful.StatefulGatewayRepository;
+import net.luminis.liq.log.LogEvent;
+import net.luminis.liq.log.LogDescriptor;
+import net.luminis.liq.server.log.store.LogStore;
+
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.Filter;
+import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.framework.Version;
+import org.osgi.service.event.Event;
+import org.osgi.service.event.EventAdmin;
+import org.osgi.service.event.EventHandler;
+import org.osgi.service.log.LogService;
+
+/**
+ * Implements the StatefulGatewayRepository. If an <code>AuditLogStore</code> is present,
+ * it will be used; it is assumed that the auditlog store is up to date.
+ */
+public class StatefulGatewayRepositoryImpl implements StatefulGatewayRepository, EventHandler {
+    private BundleContext m_context; /*Injected by dependency manager*/
+    private ArtifactRepository m_artifactRepository; /*Injected by dependency manager*/
+    private GatewayRepository m_gatewayRepository; /*Injected by dependency manager*/
+    private DeploymentVersionRepository m_deploymentRepository; /*Injected by dependency manager*/
+    private LogStore m_auditLogStore; /*Injected by dependency manager*/
+    private EventAdmin m_eventAdmin; /*Injected by dependency manager*/
+    private LogService m_log; /*Injected by dependency manager*/
+    private BundleHelper m_bundleHelper; /*Injected by dependency manager*/
+    //TODO: Make the concurrencyLevel of this concurrent hashmap settable?
+    private Map<String, StatefulGatewayObjectImpl> m_repository = new ConcurrentHashMap<String, StatefulGatewayObjectImpl>();
+
+    public StatefulGatewayObject create(Map<String, String> attributes, Map<String, String> tags) throws IllegalArgumentException {
+        throw new UnsupportedOperationException("Creating StatefulGatewayObjects is not supported.");
+    }
+
+    public List<StatefulGatewayObject> get() {
+        synchronized(m_repository) {
+            List<StatefulGatewayObject> result = new ArrayList<StatefulGatewayObject>();
+            for (StatefulGatewayObjectImpl sgoi : m_repository.values()) {
+                result.add(sgoi);
+            }
+            return result;
+        }
+    }
+
+    public List<StatefulGatewayObject> get(Filter filter) {
+        synchronized(m_repository) {
+            List<StatefulGatewayObject> result = new ArrayList<StatefulGatewayObject>();
+            for (StatefulGatewayObject entry : m_repository.values()) {
+                if (filter.match(entry.getDictionary())) {
+                    result.add(entry);
+                }
+            }
+            return result;
+        }
+    }
+
+    public void remove(StatefulGatewayObject entity) {
+        throw new UnsupportedOperationException("Removing StatefulGatewayObjects is not supported.");
+    }
+
+    public StatefulGatewayObject preregister(Map<String, String> attributes, Map<String, String> tags) {
+        synchronized(m_repository) {
+            GatewayObject go = m_gatewayRepository.create(attributes, tags);
+            return createStateful(go.getID());
+        }
+    }
+
+    public void unregister(String gatewayID) {
+        synchronized(m_repository) {
+            GatewayObject go = getGatewayObject(gatewayID);
+            if (go == null) {
+                throw new IllegalArgumentException(gatewayID + " does not represent a GatewayObject.");
+            }
+            else {
+                m_gatewayRepository.remove(go);
+                // No need to inform the stateful representation; this will be done by the event handler.
+            }
+        }
+    }
+
+    public void refresh() {
+        populate();
+    }
+
+    /**
+     * Gets the <code>GatewayObject</code> which is identified by the <code>gatewayID</code>.
+     * @param gatewayID A string representing a gateway ID.
+     * @return The <code>GatewayObject</code> from the <code>GatewayRepository</code> which has the given
+     * ID, or <code>null</code> if none can be found.
+     */
+    GatewayObject getGatewayObject(String gatewayID) {
+//        synchronized(m_repository) {
+            try {
+                List<GatewayObject> gateways = m_gatewayRepository.get(m_context.createFilter("(" + GatewayObject.KEY_ID + "=" + RepositoryUtil.escapeFilterValue(gatewayID) + ")"));
+                if ((gateways != null) && (gateways.size() == 1)) {
+                    return gateways.get(0);
+                }
+                else {
+                    return null;
+                }
+            }
+            catch (InvalidSyntaxException e) {
+                // The filter syntax is illegal, probably a bad gateway ID.
+                return null;
+            }
+//        }
+    }
+
+    /**
+     * Gets the stateful representation of the given gateway ID.
+     * @param gatewayID A string representing a gateway ID.
+     * @return The <code>StatefulGatewayObjectImpl</code> which handles the given ID,
+     * or <code>null</code> if none can be found.
+     */
+    StatefulGatewayObjectImpl getStatefulGatewayObject(String gatewayID) {
+        synchronized(m_repository) {
+            return m_repository.get(gatewayID);
+        }
+    }
+
+    /**
+     * Creates and registers a new stateful gateway object based on the given ID.
+     * @param gatewayID A string representing a gateway ID.
+     * @return The newly created and registered <code>StatefulGatewayObjectImpl</code>.
+     */
+    private StatefulGatewayObjectImpl createStateful(String gatewayID) {
+        synchronized(m_repository) {
+            StatefulGatewayObjectImpl result = new StatefulGatewayObjectImpl(this, gatewayID);
+            if (add(result)) {
+                return result;
+            }
+            else {
+                throw new IllegalArgumentException("The StateGatewayObject " + gatewayID + " already exists.");
+            }
+        }
+    }
+
+    /**
+     * Removes the given entity from this object's repository, and notifies
+     * interested parties of this.
+     * @param entity The StatefulGatewayObjectImpl to be removed.
+     */
+    void removeStateful(StatefulGatewayObjectImpl entity) {
+        synchronized(m_repository) {
+            m_repository.remove(entity.getID());
+            notifyChanged(entity, StatefulGatewayObject.TOPIC_REMOVED);
+        }
+    }
+
+    /**
+     * Adds the given stateful object to this object's repository, and notifies
+     * interested parties of this change.
+     * @param sgoi A <code>StatefulGatewayObjectImpl</code> to be registered.
+     * @return <code>true</code> when this object has been added to the repository
+     * and listeners have been notified, <code>false</code> otherwise.
+     */
+    boolean add(StatefulGatewayObjectImpl sgoi) {
+        if (!m_repository.containsKey(sgoi)) {
+            m_repository.put(sgoi.getID(), sgoi);
+            notifyChanged(sgoi, StatefulGatewayObject.TOPIC_ADDED);
+            return true;
+        }
+        return false;
+    }
+
+    private Comparator<LogEvent> m_auditEventComparator = new Comparator<LogEvent>() {
+        public int compare(LogEvent left, LogEvent right) {
+            if (left.getLogID() == right.getLogID()) {
+                return (int) (left.getTime() - right.getTime());
+            }
+            else {
+                return (int) (left.getLogID() - right.getLogID());
+            }
+        }
+    };
+
+    /**
+     * Gets all auditlog events which are related to a given gateway ID.
+     * @param gatewayID A string representing a gateway ID.
+     * @return a list of <code>AuditEvent</code>s related to this gateway ID,
+     * ordered in the order they happened. If no events can be found, and empty list will be returned.
+     */
+    List<LogEvent> getAuditEvents(String gatewayID) {
+        return getAuditEvents(getAllDescriptors(gatewayID));
+    }
+
+    /**
+     * Gets all auditlog descriptors which are related to a given gateway.
+     * @param gatewayID The gateway ID
+     * @return A list of LogDescriptors, in no particular order.
+     */
+    List<LogDescriptor> getAllDescriptors(String gatewayID) {
+        List<LogDescriptor> result = new ArrayList<LogDescriptor>();
+        try {
+            List<LogDescriptor> descriptors = m_auditLogStore.getDescriptors(gatewayID);
+            if (descriptors != null) {
+                result = descriptors;
+            }
+        }
+        catch (IOException e) {
+            // Too bad, but not much we can do.
+            m_log.log(LogService.LOG_INFO, "Error getting descriptors from auditlog store: ", e);
+        }
+        return result;
+    }
+
+    /**
+     * Gets all audit log events for a gateway is has not yet 'seen'.
+     * @param all A list of all <code>LogDescriptor</code> from which to filter
+     * the new ones.
+     * @param seen A list of <code>LogDescriptor</code> objects, which indicate
+     * the items the gateway has already processed.
+     * @return All AuditLog events that are in the audit store, but are not identified
+     * by <code>oldDescriptors</code>, ordered by 'happened-before'.
+     */
+    List<LogEvent> getAuditEvents(List<LogDescriptor> events) {
+        // Get all events from the audit log store, if possible.
+        List<LogEvent> result = new ArrayList<LogEvent>();
+        for (LogDescriptor l : events) {
+            try {
+                result.addAll(m_auditLogStore.get(l));
+            }
+            catch (IOException e) {
+                // too bad, but not much to do.
+                m_log.log(LogService.LOG_INFO, "Error getting contents from auditlog store: ", e);
+            }
+        }
+
+        Collections.sort(result, m_auditEventComparator);
+        return result;
+    }
+
+    List<LogDescriptor> diffLogDescriptorLists(List<LogDescriptor> all, List<LogDescriptor> seen) {
+        List<LogDescriptor> descriptors = new ArrayList<LogDescriptor>();
+
+        // Find out what events should be returned
+        for (LogDescriptor s : all) {
+            LogDescriptor diffs = s;
+            for (LogDescriptor d : seen) {
+                if ((s.getLogID() == d.getLogID()) && (s.getGatewayID().equals(d.getGatewayID()))) {
+                    diffs = new LogDescriptor(s.getGatewayID(), s.getLogID(), d.getRangeSet().diffDest(s.getRangeSet()));
+                }
+            }
+            descriptors.add(diffs);
+        }
+        return descriptors;
+    }
+
+    /**
+     * See {@link DeploymentRepository#getDeploymentVersion(java.lang.String)}.
+     */
+    DeploymentVersionObject getMostRecentDeploymentVersion(String gatewayID) {
+        return m_deploymentRepository.getMostRecentDeploymentVersion(gatewayID);
+    }
+
+    /**
+     * Based on the information in this stateful object, creates a <code>GatewayObject</code>
+     * in the <code>GatewayRepository</code>.
+     * This function is intended to be used for gateways which are not yet represented
+     * in the <code>GatewayRepository</code>; if they already are, an <code>IllegalArgumentException</code>
+     * will be thrown.
+     * @param gatewayID A string representing the ID of the new gateway.
+     */
+    void register(String gatewayID) {
+        Map<String, String> attr = new HashMap<String, String>();
+        attr.put(GatewayObject.KEY_ID, gatewayID);
+        Map<String, String> tags = new HashMap<String, String>();
+        m_gatewayRepository.create(attr, tags);
+        getStatefulGatewayObject(gatewayID).updateGatewayObject(false);
+    }
+
+    /**
+     * Notifies interested parties of a change to a <code>StatefulGatewayObject</code>.
+     * @param sgoi The <code>StatefulGatewayObject</code> which has changed.
+     * @param topic A topic string for posting the event.
+     * @param additionalProperties A Properties event, already containing some extra properties. If
+     * RepositoryObject.EVENT_ENTITY is used, it will be overwritten.
+     */
+    void notifyChanged(StatefulGatewayObject sgoi, String topic, Properties additionalProperties) {
+        additionalProperties.put(RepositoryObject.EVENT_ENTITY, sgoi);
+        m_eventAdmin.postEvent(new Event(topic, additionalProperties));
+    }
+
+    /**
+     * Notifies interested parties of a change to a <code>StatefulGatewayObject</code>.
+     * @param sgoi The <code>StatefulGatewayObject</code> which has changed.
+     * @param topic A topic string for posting the event.
+     */
+    void notifyChanged(StatefulGatewayObject sgoi, String topic) {
+        notifyChanged(sgoi, topic, new Properties());
+    }
+
+    /**
+     * Reads the information sources to generate the stateful objects.
+     */
+    private void populate() {
+        synchronized(m_repository) {
+            List<StatefulGatewayObjectImpl> touched = new ArrayList<StatefulGatewayObjectImpl>();
+            touched.addAll(parseGatewayRepository());
+            touched.addAll(parseAuditLog());
+
+            // Now, it is possible we have not touched all objects. Find out which these are, and make
+            // them check whether they should still exist.
+            List<StatefulGatewayObjectImpl> all = new ArrayList<StatefulGatewayObjectImpl>(m_repository.values());
+            all.removeAll(touched);
+            for (StatefulGatewayObjectImpl sgoi : all) {
+                sgoi.updateGatewayObject(false);
+                sgoi.updateDeploymentVersions(null);
+                sgoi.updateAuditEvents(true);
+            }
+            // Furthermore, for all those we _did_ see, we need to make sure their deployment versions
+            // are up to date.
+            for (StatefulGatewayObjectImpl sgoi : touched) {
+                sgoi.updateDeploymentVersions(null);
+                sgoi.updateGatewayObject(true);
+            }
+        }
+    }
+
+    /**
+     * Checks all inhabitants of the <code>GatewayRepository</code> to see
+     * whether we already have a stateful representation of them.
+     * @param needsVerify states whether the objects which are 'touched' by this
+     * actions should verify their existence.
+     * @return A list of all the gateway objects that have been touched by this action.
+     */
+    private List<StatefulGatewayObjectImpl> parseGatewayRepository() {
+        List<StatefulGatewayObjectImpl> result = new ArrayList<StatefulGatewayObjectImpl>();
+        for (GatewayObject go : m_gatewayRepository.get()) {
+            StatefulGatewayObjectImpl sgoi = getStatefulGatewayObject(go.getID());
+            if (sgoi == null) {
+                result.add(createStateful(go.getID()));
+            }
+            else {
+                result.add(sgoi);
+                sgoi.updateGatewayObject(false);
+            }
+        }
+        return result;
+    }
+
+    /**
+     * Checks the audit log to see whether we already have a
+     * stateful object for all gateways mentioned there.
+     * @param needsVerify states whether the objects which are 'touched' by this
+     * actions should verify their existence.
+     */
+    private List<StatefulGatewayObjectImpl> parseAuditLog() {
+        List<StatefulGatewayObjectImpl> result = new ArrayList<StatefulGatewayObjectImpl>();
+        List<LogDescriptor> descriptors = null;
+        try {
+            descriptors = m_auditLogStore.getDescriptors();
+        }
+        catch (IOException e) {
+            // Not much to do.
+        }
+        if (descriptors == null) {
+            // There is no audit log available, or it failed getting the logdescriptors.
+            return result;
+        }
+
+        Set<String> gatewayIDs = new HashSet<String>();
+        for (LogDescriptor l : descriptors) {
+            gatewayIDs.add(l.getGatewayID());
+        }
+
+        /* Note: the parsing of the audit log and the creation/notification of the
+         * stateful objects has been separated, to prevent calling updateAuditEvents()
+         * multiple times on gateways which have more than one log.
+         */
+        synchronized(m_repository) {
+            for (String gatewayID : gatewayIDs) {
+                StatefulGatewayObjectImpl sgoi = getStatefulGatewayObject(gatewayID);
+                if (sgoi == null) {
+                    result.add(createStateful(gatewayID));
+                }
+                else {
+                    result.add(sgoi);
+                    sgoi.updateAuditEvents(false);
+                }
+            }
+        }
+        return result;
+    }
+
+    /**
+     * Approves the changes that will happen to the gateway based on the
+     * changes in the shop by generating a new deployment version.
+     * @param gatewayID A string representing a gateway ID.
+     * @return The version identifier of the new deployment package.
+     * @throws IOException When there is a problem generating the deployment version.
+     */
+    String approve(String gatewayID) throws IOException {
+        return generateDeploymentVersion(gatewayID).getVersion();
+    }
+
+    /**
+     * Generates an array of bundle URLs which have to be deployed on
+     * the gateway, given the current state of the shop.
+     * TODO: In the future, we want to add support for multiple shops.
+     * TODO: Is this prone to concurrency issues with changes license- and
+     * group objects?
+     * @param gatewayID A string representing a gateway.
+     * @return An array of artifact URLs.
+     * @throws IOException When there is a problem processing an artifact for deployment.
+     */
+    DeploymentArtifact[] getNecessaryDeploymentArtifacts(String gatewayID, String version) throws IOException {
+        GatewayObject go = getGatewayObject(gatewayID);
+
+        Map<ArtifactObject, String> bundles = new HashMap<ArtifactObject, String>();
+        Map<ArtifactObject, String> artifacts = new HashMap<ArtifactObject, String>();
+
+        // First, find all basic bundles and artifacts. An while we're traversing the
+        // tree of objects, build the tree of properties.
+        if (go != null) {
+            for (LicenseObject license : go.getLicenses()) {
+                for (GroupObject group : license.getGroups()) {
+                    for (ArtifactObject artifact : group.getArtifacts()) {
+                        if (m_bundleHelper.canUse(artifact)) {
+                            bundles.put(artifact, m_bundleHelper.getResourceProcessorPIDs(artifact));
+                        }
+                        else {
+                            artifacts.put(artifact, artifact.getProcessorPID());
+                        }
+                    }
+                }
+            }
+        }
+
+        // Find all processors
+        Map<String, ArtifactObject> allProcessors = new HashMap<String, ArtifactObject>();
+        for (ArtifactObject bundle : m_artifactRepository.getResourceProcessors()) {
+            allProcessors.put(m_bundleHelper.getResourceProcessorPIDs(bundle), bundle);
+        }
+
+        // Determine all resource processors we need
+        for (String processor : artifacts.values()) {
+            if (!bundles.containsValue(processor)) {
+                ArtifactObject bundle = allProcessors.get(processor);
+                if (bundle == null) {
+                    m_log.log(LogService.LOG_ERROR, "Unable to create deployment version: there is no resource processing bundle available that publishes " + processor);
+                    throw new IllegalStateException("Unable to create deployment version: there is no resource processing bundle available that publishes " + processor);
+                }
+                bundles.put(bundle, processor);
+            }
+        }
+
+        List<DeploymentArtifact> result = new ArrayList<DeploymentArtifact>();
+
+        for (ArtifactObject bundle : bundles.keySet()) {
+            Map<String, String> directives = new HashMap<String, String>();
+            if (m_bundleHelper.isResourceProcessor(bundle)) {
+                // it's a resource processor, mark it as such.
+                directives.put(DeploymentArtifact.DIRECTIVE_ISCUSTOMIZER, "true");
+            }
+            directives.put(BundleHelper.KEY_SYMBOLICNAME, m_bundleHelper.getSymbolicName(bundle));
+            String bundleVersion = m_bundleHelper.getVersion(bundle);
+            if (bundleVersion != null) {
+                directives.put(BundleHelper.KEY_VERSION, bundleVersion);
+            }
+            directives.put(DeploymentArtifact.DIRECTIVE_KEY_BASEURL, bundle.getURL());
+            result.add(m_deploymentRepository.createDeploymentArtifact(bundle.getURL(), directives));
+        }
+
+        for (ArtifactObject artifact : artifacts.keySet()) {
+            Map<String, String> directives = new HashMap<String, String>();
+            directives.put(DeploymentArtifact.DIRECTIVE_KEY_PROCESSORID, artifact.getProcessorPID());
+            directives.put(DeploymentArtifact.DIRECTIVE_KEY_BASEURL, artifact.getURL());
+            result.add(m_deploymentRepository.createDeploymentArtifact(m_artifactRepository.preprocessArtifact(artifact, go, gatewayID, version), directives));
+        }
+
+        return result.toArray(new DeploymentArtifact[result.size()]);
+    }
+
+    /**
+     * Quick method to find all artifacts that need to be deployed to a gateway.
+    */
+    ArtifactObject[] getNecessaryArtifacts(String gatewayID) {
+        List<ArtifactObject> result = new ArrayList<ArtifactObject>();
+        GatewayObject go = getGatewayObject(gatewayID);
+
+        Map<String, ArtifactObject> allProcessors = new HashMap<String, ArtifactObject>();
+        for (ArtifactObject bundle : m_artifactRepository.getResourceProcessors()) {
+            allProcessors.put(m_bundleHelper.getResourceProcessorPIDs(bundle), bundle);
+        }
+
+        if (go != null) {
+            for (LicenseObject license : go.getLicenses()) {
+                for (GroupObject group : license.getGroups()) {
+                    for (ArtifactObject artifact : group.getArtifacts()) {
+                        result.add(artifact);
+                        if (!m_bundleHelper.canUse(artifact)) {
+                            ArtifactObject processor = allProcessors.get(artifact.getProcessorPID());
+                            if (processor == null) {
+                                // this means we cannot create a useful version; return null.
+                                return null;
+                            }
+                            result.add(processor);
+                        }
+                    }
+                }
+            }
+        }
+
+        return result.toArray(new ArtifactObject[result.size()]);
+    }
+
+    /**
+     * Generates a new deployment version for the the given gateway,
+     * based on the bundles it is linked to by the licenses it is
+     * associated to.
+     * @param gatewayID A string representing a gateway.
+     * @return A new DeploymentVersionObject, representing this new version for the gateway.
+     * @throws IOException When there is a problem determining the artifacts to be deployed.
+     */
+    DeploymentVersionObject generateDeploymentVersion(String gatewayID) throws IOException {
+        Map<String, String> attr = new HashMap<String, String>();
+        attr.put(DeploymentVersionObject.KEY_GATEWAYID, gatewayID);
+        Map<String, String> tags = new HashMap<String, String>();
+
+        DeploymentVersionObject mostRecentDeploymentVersion = getMostRecentDeploymentVersion(gatewayID);
+        String nextVersion;
+        if (mostRecentDeploymentVersion == null) {
+            nextVersion = nextVersion(null);
+        }
+        else {
+            nextVersion = nextVersion(mostRecentDeploymentVersion.getVersion());
+        }
+        attr.put(DeploymentVersionObject.KEY_VERSION, nextVersion);
+
+        synchronized(m_repository) {
+            DeploymentVersionObject result = m_deploymentRepository.create(attr, tags, getNecessaryDeploymentArtifacts(gatewayID, nextVersion));
+
+            StatefulGatewayObjectImpl sgoi = getStatefulGatewayObject(gatewayID);
+            if (sgoi == null) {
+                createStateful(gatewayID);
+            }
+            else {
+                sgoi.updateDeploymentVersions(result);
+            }
+
+            return result;
+        }
+    }
+
+    /**
+     * Generates the next version, based on the version passed in.
+     * The version is assumed to be an OSGi-version; for now, the next
+     * 'major' version is generated. In the future, we might want to do
+     * 'smarter' things here, like checking the impact of a new version
+     * and use the minor and micro versions, or attach some qualifier.
+     * @param version A string representing a deployment version's version.
+     * @return A string representing the next version.
+     */
+    private static String nextVersion(String version) {
+        try {
+            Version v = new Version(version);
+            Version result = new Version(v.getMajor() + 1, 0, 0);
+            return result.toString();
+        }
+        catch (Exception iae) {
+            // Basically, if anything goes wrong, we assume we want to start a new version at 1.
+            return "1.0.0";
+        }
+    }
+
+    public void handleEvent(Event event) {
+        if (event.getTopic().equals(GatewayObject.TOPIC_ADDED) || event.getTopic().equals(GatewayObject.TOPIC_REMOVED)) {
+            synchronized(m_repository) {
+                String id = ((GatewayObject) event.getProperty(RepositoryObject.EVENT_ENTITY)).getID();
+                StatefulGatewayObjectImpl sgoi = getStatefulGatewayObject(id);
+                if (sgoi == null) {
+                    createStateful(id);
+                }
+                else {
+                    sgoi.updateGatewayObject(true);
+                }
+            }
+        }
+        else if (event.getTopic().equals(DeploymentVersionObject.TOPIC_ADDED) || event.getTopic().equals(DeploymentVersionObject.TOPIC_REMOVED)) {
+            synchronized(m_repository) {
+                DeploymentVersionObject deploymentVersionObject = ((DeploymentVersionObject) event.getProperty(RepositoryObject.EVENT_ENTITY));
+                String id = deploymentVersionObject.getGatewayID();
+                StatefulGatewayObjectImpl sgoi = getStatefulGatewayObject(id);
+                if (sgoi == null) {
+                    createStateful(id);
+                }
+                else {
+                    sgoi.updateDeploymentVersions(deploymentVersionObject);
+                }
+            }
+        }
+        else if (event.getTopic().equals(RepositoryAdmin.TOPIC_LOGIN)) {
+            synchronized(m_repository) {
+                populate();
+            }
+        }
+        else if (event.getTopic().equals(RepositoryAdmin.TOPIC_REFRESH)) {
+            synchronized(m_repository) {
+                populate();
+            }
+        }
+        else {
+            // Something else has changed; however, the entire shop may have an influence on
+            // any gateway, so recheck everything.
+            synchronized(m_repository) {
+                for (StatefulGatewayObjectImpl sgoi : m_repository.values()) {
+                    sgoi.determineStatus();
+                }
+            }
+        }
+    }
+
+    boolean needsNewVersion(ArtifactObject artifact, String gatewayID, String version) {
+        return m_artifactRepository.needsNewVersion(artifact, getGatewayObject(gatewayID), gatewayID, version);
+    }
+}

Added: incubator/ace/trunk/server/src/net/luminis/liq/client/repositoryuseradmin/RepositoryUserAdmin.java
URL: http://svn.apache.org/viewvc/incubator/ace/trunk/server/src/net/luminis/liq/client/repositoryuseradmin/RepositoryUserAdmin.java?rev=788992&view=auto
==============================================================================
--- incubator/ace/trunk/server/src/net/luminis/liq/client/repositoryuseradmin/RepositoryUserAdmin.java (added)
+++ incubator/ace/trunk/server/src/net/luminis/liq/client/repositoryuseradmin/RepositoryUserAdmin.java Sat Jun 27 15:53:04 2009
@@ -0,0 +1,61 @@
+package net.luminis.liq.client.repositoryuseradmin;
+
+import java.io.IOException;
+import java.net.URL;
+
+import org.osgi.service.useradmin.User;
+import org.osgi.service.useradmin.UserAdmin;
+
+/**
+ * RepositoryUserAdmin is used for managing a User Admin repository
+ * that is present on a server. It uses the UserAdmin interface to
+ * allow alterations. Any non-supported functions from UserAdmin 
+ * (or its related classes) will result in a {@link UnsupportedOperationException}.<br>
+ * <br>
+ * This service uses the same checkout/commit/revert scheme that
+ * RepositoryAdmin does; when making changes, they will always be stored locally,
+ * but they will only be updated on the server once commit is called.<br>
+ * <br>
+ * Note that this implementation will <b>not</b> send any events.
+ */
+public interface RepositoryUserAdmin extends UserAdmin {
+
+    /**
+     * Logs in to a specific repository location.
+     * @param user A user object to use in the connection
+     * @param repositoryLocation A URL representing the base URL of the repository service
+     * @param repositoryCustomer The 'customer' for which the repository is registered
+     * @param repositoryName The 'name' for which the repository is registered
+     * @param writeAccess <code>true</code> if write-access is required, <code>false</code> otherwise.
+     * @throws IOException Thrown when there is a problem handling the backup files.
+     */
+    public void login(User user, URL repositoryLocation, String repositoryCustomer, String repositoryName) throws IOException;
+    
+    /**
+     * Logs out the user.
+     * @param force Even when something goes wrong, force a logout.
+     * @throws IOException When there is a problem writing the current status to local storage.
+     */
+    public void logout(boolean force) throws IOException;
+    
+    /**
+     * Checks out the latest version from the server. If any changes exist, they will
+     * be reflected in this service's user admin.
+     * @throws IOException If there is a problem communicating with the server.
+     */
+    public void checkout() throws IOException;
+
+    /**
+     * Writes all changes made to this service's user admin to the server.
+     * @throws IOException If there is a problem communicating with the server.
+     */
+    public void commit() throws IOException;
+
+    /**
+     * Undoes all changes to this service's user admin, and restores the previously
+     * checked out or committed version.
+     * @throws IOException If there is a problem retrieving the data from the
+     * local backup.
+     */
+    public void revert() throws IOException;
+}

Added: incubator/ace/trunk/server/src/net/luminis/liq/client/repositoryuseradmin/impl/Activator.java
URL: http://svn.apache.org/viewvc/incubator/ace/trunk/server/src/net/luminis/liq/client/repositoryuseradmin/impl/Activator.java?rev=788992&view=auto
==============================================================================
--- incubator/ace/trunk/server/src/net/luminis/liq/client/repositoryuseradmin/impl/Activator.java (added)
+++ incubator/ace/trunk/server/src/net/luminis/liq/client/repositoryuseradmin/impl/Activator.java Sat Jun 27 15:53:04 2009
@@ -0,0 +1,39 @@
+package net.luminis.liq.client.repositoryuseradmin.impl;
+
+import net.luminis.liq.client.repositoryuseradmin.RepositoryUserAdmin;
+
+import org.apache.felix.dependencymanager.DependencyActivatorBase;
+import org.apache.felix.dependencymanager.DependencyManager;
+import org.osgi.framework.BundleContext;
+import org.osgi.service.log.LogService;
+import org.osgi.service.prefs.PreferencesService;
+
+/**
+ * Activator for the Repository UserAdmin. Note that this UserAdmin is not intended
+ * to be a full implementation of the UserAdmin specification, but rather a
+ * value-object model that uses the UserAdmin interface for convenience.
+ */
+public class Activator extends DependencyActivatorBase {
+
+    RepositoryUserAdminImpl m_impl;
+
+    @Override
+    public void init(BundleContext context, DependencyManager manager) {
+        m_impl = new RepositoryUserAdminImpl();
+        manager.add(createService()
+            .setInterface(RepositoryUserAdmin.class.getName(), null)
+            .setImplementation(m_impl)
+            .add(createServiceDependency()
+                 .setService(PreferencesService.class)
+                 .setRequired(true))
+            .add(createServiceDependency()
+                 .setService(LogService.class)
+                 .setRequired(false)));
+    }
+
+    @Override
+    public void destroy(BundleContext context, DependencyManager manager) throws Exception {
+        // At least, save our progress.
+        m_impl.logout(true);
+    }
+}

Added: incubator/ace/trunk/server/src/net/luminis/liq/client/repositoryuseradmin/impl/RepositoryUserAdminImpl.java
URL: http://svn.apache.org/viewvc/incubator/ace/trunk/server/src/net/luminis/liq/client/repositoryuseradmin/impl/RepositoryUserAdminImpl.java?rev=788992&view=auto
==============================================================================
--- incubator/ace/trunk/server/src/net/luminis/liq/client/repositoryuseradmin/impl/RepositoryUserAdminImpl.java (added)
+++ incubator/ace/trunk/server/src/net/luminis/liq/client/repositoryuseradmin/impl/RepositoryUserAdminImpl.java Sat Jun 27 15:53:04 2009
@@ -0,0 +1,616 @@
+package net.luminis.liq.client.repositoryuseradmin.impl;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.PipedInputStream;
+import java.io.PipedOutputStream;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Dictionary;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Hashtable;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
+
+import net.luminis.liq.client.repositoryuseradmin.RepositoryUserAdmin;
+import net.luminis.liq.repository.Repository;
+import net.luminis.liq.repository.ext.CachedRepository;
+import net.luminis.liq.repository.impl.CachedRepositoryImpl;
+import net.luminis.liq.repository.impl.FilebasedBackupRepository;
+import net.luminis.liq.repository.impl.RemoteRepository;
+
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.Filter;
+import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.service.log.LogService;
+import org.osgi.service.prefs.Preferences;
+import org.osgi.service.prefs.PreferencesService;
+import org.osgi.service.useradmin.Authorization;
+import org.osgi.service.useradmin.Role;
+import org.osgi.service.useradmin.User;
+
+import com.thoughtworks.xstream.XStream;
+import com.thoughtworks.xstream.converters.Converter;
+import com.thoughtworks.xstream.converters.MarshallingContext;
+import com.thoughtworks.xstream.converters.UnmarshallingContext;
+import com.thoughtworks.xstream.io.HierarchicalStreamReader;
+import com.thoughtworks.xstream.io.HierarchicalStreamWriter;
+import com.thoughtworks.xstream.io.StreamException;
+import com.thoughtworks.xstream.io.xml.DomDriver;
+
+/**
+ * RepositoryUserAdminImpl can checkout, commit and revert a repository
+ * containing user data. It uses XStream to read and write the data.
+ */
+public class RepositoryUserAdminImpl implements RepositoryUserAdmin {
+
+    private static final String REPOSITORY_USER_ADMIN_PREFS = "repositoryUserAdminPrefs";
+    private static final String PREFS_LOCAL_FILE_ROOT = "repositoryUserAdmin";
+    private static final String PREFS_LOCAL_FILE_LOCATION = "FileLocation";
+    private static final String PREFS_LOCAL_FILE_CURRENT = "current";
+    private static final String PREFS_LOCAL_FILE_BACKUP = "backup";
+
+    private volatile BundleContext m_context;
+    private volatile LogService m_log;
+    private volatile PreferencesService m_preferences;
+
+    private final Map<String, RoleImpl> m_roles = new ConcurrentHashMap<String, RoleImpl>();
+    private CachedRepository m_repository;
+    /**
+     * Lock to be used when making changes to m_repository.
+     */
+    private final Object m_repositoryLock = new Object();
+    private Preferences m_repositoryPrefs;
+
+    public void login(User user, URL repositoryLocation, String repositoryCustomer, String repositoryName) throws IOException {
+        synchronized(m_repositoryLock) {
+            // Create our own backup repository
+            RemoteRepository remote = new RemoteRepository(repositoryLocation, repositoryCustomer, repositoryName);
+            m_repositoryPrefs = getUserPrefs(user, repositoryLocation, repositoryCustomer, repositoryName);
+            m_repository = getCachedRepositoryFromPreferences(user, remote);
+
+            // Fill the store with any data that might be available locally
+            try {
+                read(m_repository.getLocal(true));
+            }
+            catch (IOException ioe) {
+                // TODO why is this logged as an error when it occurs when there simply is no data?
+                m_log.log(LogService.LOG_ERROR, "Error retrieving local data.", ioe);
+            }
+        }
+    }
+
+    public void logout(boolean force) throws IOException {
+        // logout stores the data locally, ready for the next run
+        synchronized(m_repositoryLock) {
+            if (!force) {
+                ensureLoggedin();
+            }
+            try {
+                writeLocal();
+            }
+            catch (IOException ioe) {
+                if (!force) {
+                    throw ioe;
+                }
+            }
+            catch (RuntimeException re) {
+                if (!force) {
+                    throw re;
+                }
+            }
+            m_repository = null;
+        }
+    }
+
+    public void checkout() throws IOException {
+        synchronized(m_repositoryLock) {
+            ensureLoggedin();
+            read(m_repository.checkout(false));
+            storeVersion();
+        }
+    }
+
+    public void commit() throws IOException {
+        synchronized(m_repositoryLock) {
+            ensureLoggedin();
+            // First write to the local store, and then commit it
+            writeLocal();
+            m_repository.commit();
+            storeVersion();
+        }
+    }
+
+    /**
+     * Helper method to write out the contents of the RepositoryUserAdminImpl to
+     * a repository. This method will create a new thread to do the writing, and
+     * wait for the thread to be ready.
+     * @throws IOException Thrown when either this thread, or the thread that is
+     * started to do the writing, throws an exception.
+     */
+    private void writeLocal() throws IOException {
+        PipedInputStream in = new PipedInputStream();
+        final PipedOutputStream out = new PipedOutputStream(in);
+        final Semaphore semaphore = new Semaphore(0);
+        final Exception[] exceptions = new Exception[1];
+        new Thread("RepositoryUserAdmin writer") {
+            @Override
+            public void run() {
+                try {
+                    write(out);
+                }
+                catch (IOException e) {
+                    m_log.log(LogService.LOG_ERROR, "Error writing out contents of RepositoryAdminUser", e);
+                    exceptions[0] = e;
+                }
+                catch (IllegalArgumentException iae) {
+                    m_log.log(LogService.LOG_ERROR, "Error writing out contents of RepositoryAdminUser", iae);
+                    exceptions[0] = iae;
+                }
+                semaphore.release();
+            }
+        }.start();
+        m_repository.writeLocal(in);
+        try {
+            if (!semaphore.tryAcquire(30, TimeUnit.SECONDS)) {
+                throw new IOException("Error writing the contents of RepositoryUserAdmin.");
+            }
+        }
+        catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+        }
+        if (exceptions[0] != null) {
+            if (exceptions[0] instanceof IOException) {
+                throw (IOException) exceptions[0];
+            }
+            if (exceptions[0] instanceof RuntimeException) {
+                throw (RuntimeException) exceptions[0];
+            }
+        }
+    }
+
+    public void revert() throws IOException {
+        synchronized(m_repositoryLock) {
+            ensureLoggedin();
+            m_repository.revert();
+            read(m_repository.getLocal(false));
+        }
+    }
+
+    /**
+     * Makes sure a user is logged in before 'stuff' can be done. Make sure the
+     * calling thread is holding the m_repositoryLock.
+     */
+    private void ensureLoggedin() {
+        if (m_repository == null) {
+            throw new IllegalStateException("This operation requires a user to be logged in.");
+        }
+    }
+
+    /**
+     * Reads the content of the stream, and updates this service's
+     * contents accordingly. The caller of this method should hold the
+     * m_repositoryLock.
+     */
+    @SuppressWarnings("unchecked")
+    private void read(InputStream input) {
+        m_roles.clear();
+        // We use DomDriver because the standard XPP driver has issues with attributes.
+        XStream xstream = new XStream(/*new DomDriver()*/);
+        xstream.registerConverter(ROLEMAPCONVERTER);
+        xstream.registerConverter(ROLECONVERTER);
+        xstream.registerConverter(DICTCONVERTER);
+        xstream.aliasType("roles", Map.class);
+        try {
+            Map<String, RoleImpl> fromXML = (Map<String, RoleImpl>) xstream.fromXML(input);
+            m_roles.putAll(fromXML);
+        }
+        catch (StreamException e) {
+            // no problem: this means that the remote repository is empty.
+        }
+    }
+
+    /**
+     * Writes the current contents of this service.
+     * The caller of this method should hold the m_repositoryLock.
+     * @param out An output stream to write to. It will be closed by the this method.
+     * @throws IOException When there is a problem creating the stream, or
+     * the other end of the stream fails.
+     */
+    private void write(OutputStream out) throws IOException {
+        XStream xstream = new XStream(new DomDriver());
+        xstream.registerConverter(ROLEMAPCONVERTER);
+        xstream.registerConverter(ROLECONVERTER);
+        xstream.registerConverter(DICTCONVERTER);
+        xstream.aliasType("roles", Map.class);
+        xstream.toXML(m_roles, out);
+        try {
+            out.close();
+        }
+        catch (IOException e) {
+            m_log.log(LogService.LOG_ERROR, "Error closing XStream output stream.", e);
+            throw e;
+        }
+    }
+
+    /**
+     * Gets the preferences for a user/location/customer/name combination.
+     */
+    private Preferences getUserPrefs(User user, URL location, String customer, String name) {
+        Preferences userPrefs = m_preferences.getUserPreferences(user.getName());
+        Preferences userAdminPrefs = userPrefs.node(REPOSITORY_USER_ADMIN_PREFS);
+        Preferences repoPref = userAdminPrefs.node(location.getAuthority() + location.getPath());
+        Preferences customerPref = repoPref.node(customer);
+        return customerPref.node(name);
+    }
+
+    /**
+     * Creates a cached repository based on preferences.
+     */
+    private CachedRepository getCachedRepositoryFromPreferences(User user, Repository repository) throws IOException {
+        long mostRecentVersion = m_repositoryPrefs.getLong("version", CachedRepositoryImpl.UNCOMMITTED_VERSION);
+        File current = getFileFromPreferences(PREFS_LOCAL_FILE_CURRENT);
+        File backup = getFileFromPreferences(PREFS_LOCAL_FILE_BACKUP);
+        return new CachedRepositoryImpl(user, repository, new FilebasedBackupRepository(current, backup), mostRecentVersion);
+    }
+
+    /**
+     * Writes the current version of the repository we are working on to the preferences.
+     */
+    private void storeVersion() {
+        m_repositoryPrefs.putLong("version", m_repository.getMostRecentVersion());
+    }
+
+    /**
+     * Gets a named file in preferences. If the file does not yet exist, it will
+     * be created, and its location noted in the preferences.
+     */
+    private File getFileFromPreferences(String type) throws IOException {
+        String directory = m_repositoryPrefs.get(PREFS_LOCAL_FILE_LOCATION, "");
+
+        if ((directory == "") || !m_context.getDataFile(PREFS_LOCAL_FILE_ROOT + "/" + directory).isDirectory()) {
+            if (!m_context.getDataFile(PREFS_LOCAL_FILE_ROOT + "/" + directory).isDirectory() && (directory != "")) {
+                m_log.log(LogService.LOG_WARNING, "Directory '" + directory + "' should exist according to the preferences, but it does not.");
+            }
+            // The file did not exist, so create a new one.
+            File directoryFile = null;
+            File bundleDataDir = m_context.getDataFile(PREFS_LOCAL_FILE_ROOT);
+            if (!bundleDataDir.isDirectory()) {
+                if (!bundleDataDir.mkdir()) {
+                    throw new IOException("Error creating the local repository root directory.");
+                }
+            }
+            directoryFile = File.createTempFile("repo", "", bundleDataDir);
+
+            directoryFile.delete(); // No problem if this goes wrong, it just means it wasn't there yet.
+            if (!directoryFile.mkdir()) {
+                throw new IOException("Error creating the local repository storage directory.");
+            }
+            m_repositoryPrefs.put(PREFS_LOCAL_FILE_LOCATION, directoryFile.getName());
+            return new File(directoryFile, type);
+        }
+        else {
+            // Get the given file from that location.
+            return m_context.getDataFile(PREFS_LOCAL_FILE_ROOT + "/" + directory + "/" + type);
+        }
+    }
+
+    /* ******************************
+     * The UserAdmin implementation *
+     * ******************************/
+
+    public Role createRole(String name, int type) {
+        if ((type != Role.USER) && (type != Role.GROUP)) {
+            throw new IllegalArgumentException("Type " + type + " is unknown.");
+        }
+
+        // event tough we have a ConcurrentHashMap, we still should make the checking for existence
+        // and actual creation an atomic operation.
+        synchronized (m_roles) {
+            if (m_roles.containsKey(name)) {
+                return null;
+            }
+
+            RoleImpl result = new RoleImpl(name, type);
+            m_roles.put(name, result);
+            return result;
+        }
+    }
+
+    public Authorization getAuthorization(User user) {
+        throw new UnsupportedOperationException("getAuthorization is not supported by RepositoryUserAdmin.");
+    }
+
+    public Role getRole(String name) {
+        return m_roles.get(name);
+    }
+
+    public Role[] getRoles(String filter) throws InvalidSyntaxException {
+        if (filter == null) {
+            return m_roles.values().toArray(new Role[m_roles.size()]);
+        }
+
+        Filter f = m_context.createFilter(filter);
+
+        List<Role> result = new ArrayList<Role>();
+        for (RoleImpl impl : m_roles.values()) {
+            if (f.match(impl.getProperties())) {
+                result.add(impl);
+            }
+        }
+
+        // The spec requires us to return null when we have no results.
+        return result.size() > 0 ? result.toArray(new Role[result.size()]) : null;
+    }
+
+    public User getUser(String key, String value) {
+        List<User> result = new ArrayList<User>();
+        for (Role role : m_roles.values()) {
+            if ((role.getType() == Role.USER) && value.equals(role.getProperties().get(key))) {
+                result.add((User) role);
+            }
+        }
+
+        return result.size() == 1 ? result.get(0) : null;
+    }
+
+    public boolean removeRole(String name) {
+        RoleImpl role = m_roles.remove(name);
+        if (role == null) {
+            return false;
+        }
+        for (String groupName : role.getMemberships(this)) {
+            RoleImpl group = m_roles.get(groupName);
+            if (group != null) {
+                group.removeMember(role);
+            }
+        }
+        return true;
+    }
+
+    /* ***********************
+     * Serialization helpers *
+     * ***********************/
+
+    /**
+     * XStream Converter for a Dictionary, with support for both Strings and
+     * byte[]'s as values. Resulting format:
+     * <pre>
+     * &lt;keyname1 type = "String"&gt;value1&lt;/keyname1&gt;
+     * &lt;keyname1 type = "byte[]"&gt;value1&lt;/keyname1&gt;
+     * </pre>
+     */
+    @SuppressWarnings("unchecked")
+    private static final Converter DICTCONVERTER = new Converter() {
+        public void marshal(Object object, HierarchicalStreamWriter writer, MarshallingContext context) {
+            Dictionary dict = (Dictionary) object;
+            Enumeration e = dict.keys();
+            while (e.hasMoreElements()) {
+                String key = (String) e.nextElement();
+                Object value = dict.get(key);
+                writer.startNode(key);
+                if (value instanceof String) {
+                    writer.addAttribute("type", "String");
+                    writer.setValue((String) value);
+                }
+                else if (value instanceof byte[]) {
+                    writer.addAttribute("type", "byte[]");
+                    writer.setValue(new String((byte[]) value));
+                }
+                else if (value == null) {
+                    throw new IllegalArgumentException("Encountered a null value in the dictionary for key " + key);
+                }
+                else {
+                    throw new IllegalArgumentException("The dictionary contains a non-recognized value " + value.getClass().getName() + " for key " + key);
+                }
+                writer.endNode();
+            }
+        }
+
+        public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext converter) {
+            Dictionary result = new Hashtable<String, Object>();
+            while (reader.hasMoreChildren()) {
+                reader.moveDown();
+                Object value;
+                if ((reader.getAttribute("type") == null) || reader.getAttribute("type").equals("String")) {
+                    value = reader.getValue();
+                }
+                else if (reader.getAttribute("type").equals("byte[]")) {
+                    value = reader.getValue().getBytes();
+                }
+                else {
+                    throw new IllegalArgumentException("Encountered an unknown type tag: " + reader.getAttribute("type"));
+                }
+                result.put(reader.getNodeName(), value);
+                reader.moveUp();
+            }
+            return result;
+        }
+
+        public boolean canConvert(Class clazz) {
+            return Dictionary.class.isAssignableFrom(clazz);
+        }
+    };
+
+    /**
+     * XStream convertor for RoleImpl objects. Resulting format:
+     * <pre>
+     * &lt;user name="me"&gt;
+     *     &lt;properties&gt;
+     *     ...up to DICTCONVERTER...
+     *     &lt;/properties&gt;
+     *     &lt;credentials&gt;
+     *     ...up to DICTCONVERTER...
+     *     &lt;/credentials&gt;
+     *     &lt;memberof&gt;group1&lt;/memberof&gt;
+     *     &lt;memberof&gt;group2&lt;/memberof&gt;
+     * &lt;/user>
+     * </pre>
+     * This converter will use the context property 'deserialized' to find
+     * groups that the currently deserialized entry should be a member of.
+     */
+    @SuppressWarnings("unchecked")
+    private final Converter ROLECONVERTER = new Converter() {
+        public void marshal(Object object, HierarchicalStreamWriter writer, MarshallingContext context) {
+            RoleImpl role = (RoleImpl) object;
+
+            if (role.getType() == Role.USER) {
+                writer.startNode("user");
+            }
+            else {
+                writer.startNode("group");
+            }
+            writer.addAttribute("name", role.getName());
+
+            writer.startNode("properties");
+            context.convertAnother(role.getProperties());
+            writer.endNode();
+
+            writer.startNode("credentials");
+            context.convertAnother(role.getCredentials());
+            writer.endNode();
+
+            for (String s : role.getMemberships(RepositoryUserAdminImpl.this)) {
+                writer.startNode("memberof");
+                writer.setValue(s);
+                writer.endNode();
+            }
+
+            writer.endNode();
+        }
+
+        public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) {
+            int type;
+            if (reader.getNodeName().equals("user")) {
+                type = Role.USER;
+            }
+            else if (reader.getNodeName().equals("group")) {
+                type = Role.GROUP;
+            }
+            else {
+                throw new IllegalArgumentException("Encountered an unknown node name: " + reader.getNodeName());
+            }
+
+            RoleImpl result = new RoleImpl(reader.getAttribute("name"), type);
+
+            while (reader.hasMoreChildren()) {
+                reader.moveDown();
+                if (reader.getNodeName().equals("properties")) {
+                    copyDict(result.getProperties(), (Dictionary<String, Object>) context.convertAnother(reader, Dictionary.class));
+                }
+                else if (reader.getNodeName().equals("credentials")) {
+                    copyDict(result.getCredentials(), (Dictionary<String, Object>) context.convertAnother(reader, Dictionary.class));
+                }
+                else if (reader.getNodeName().equals("memberof")) {
+                    ((Map<String, RoleImpl>) context.get("deserialized")).get(reader.getValue()).addMember(result);
+                }
+                reader.moveUp();
+            }
+
+            return result;
+        }
+
+        /**
+         * Helper method that copies the contents of one dictionary to another.
+         */
+        private void copyDict(Dictionary to, Dictionary from) {
+            Enumeration<String> e = from.keys();
+            while (e.hasMoreElements()) {
+                String key = e.nextElement();
+                to.put(key, from.get(key));
+            }
+        }
+
+        public boolean canConvert(Class clazz) {
+            return RoleImpl.class.isAssignableFrom(clazz);
+        }
+    };
+
+    /**
+     * XStream converter for a Map which contains Roles. Resulting format:
+     * <pre>
+     * &lt;roles&gt;
+     *     ...up to ROLECONVERTER...
+     *     ...up to ROLECONVERTER...
+     * &lt;/roles&gt;
+     * </pre>
+     * This converter will use the 'deserialized' context property to store the map
+     * of already deserialized roles, so ROLECONVERTER can use that.<br>
+     * Furthermore, it uses a simple form of cycle detection when serializing.
+     */
+    private final Converter ROLEMAPCONVERTER = new Converter() {
+
+        @SuppressWarnings("unchecked")
+        public void marshal(Object object, HierarchicalStreamWriter writer, MarshallingContext context) {
+            Map<String, RoleImpl> todo = new HashMap<String, RoleImpl>();
+            todo.putAll(((Map) object));
+
+            /*
+             * We only serialize roles that have no dependencies on roles that have not yet been
+             * serialized. To do so, we check all dependencies of a role, and see whether any of these
+             * still has to be serialized. If so, we skip that role for now, and try to serialize it
+             * in a later run. We go over the list a number of times, until it stops shrinking.
+             */
+            int removed = 1;
+            while (removed != 0) {
+                // We need to store the elements we have handled separately: we cannot remove them from todo directly.
+                Set<String> done = new HashSet<String>();
+                for (RoleImpl role : todo.values()) {
+                    String[] memberships = role.getMemberships(RepositoryUserAdminImpl.this);
+                    if (!contains(memberships, todo.keySet())) {
+                        context.convertAnother(role);
+                        done.add(role.getName());
+                    }
+                }
+                for (String s : done) {
+                    todo.remove(s);
+                }
+                removed = done.size();
+            }
+            if (!todo.isEmpty()) {
+                // removed has to be 0, so no elements have been removed from todo in the previous run. However,
+                // if todo now is not empty, we know we have a circular dependency.
+                throw new IllegalArgumentException("The role tree contains a circular dependency, and cannot be serialized.");
+            }
+        }
+
+        /**
+         * @return <code>false</code> if none of the elements from subset appear in
+         * set, <code>true</code> otherwise.
+         */
+        private boolean contains(String[] subset, Set<String> set) {
+            for (String s : subset) {
+                if (set.contains(s)) {
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) {
+            Map<String, RoleImpl> result = new HashMap<String, RoleImpl>();
+            context.put("deserialized", result);
+            while (reader.hasMoreChildren()) {
+                reader.moveDown();
+                RoleImpl role = (RoleImpl) context.convertAnother(reader, RoleImpl.class);
+                result.put(role.getName(), role);
+                reader.moveUp();
+            }
+            return result;
+        }
+
+        @SuppressWarnings("unchecked")
+        public boolean canConvert(Class clazz) {
+            return Map.class.isAssignableFrom(clazz);
+        }
+    };
+
+}