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>
+ * <keyname1 type = "String">value1</keyname1>
+ * <keyname1 type = "byte[]">value1</keyname1>
+ * </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>
+ * <user name="me">
+ * <properties>
+ * ...up to DICTCONVERTER...
+ * </properties>
+ * <credentials>
+ * ...up to DICTCONVERTER...
+ * </credentials>
+ * <memberof>group1</memberof>
+ * <memberof>group2</memberof>
+ * </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>
+ * <roles>
+ * ...up to ROLECONVERTER...
+ * ...up to ROLECONVERTER...
+ * </roles>
+ * </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);
+ }
+ };
+
+}