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 2012/07/05 14:10:06 UTC

svn commit: r1357570 [19/34] - in /ace/sandbox/marrs: cnf/ cnf/ext/ cnf/lib/ cnf/releaserepo/ cnf/repo/ cnf/repo/.obrcache/ cnf/repo/.obrcache/http%3A%2F%2Fbundles.bndtools.org.s3.amazonaws.com%2Fcom.jcraft.jsch/ cnf/repo/.obrcache/http%3A%2F%2Fbundles...

Added: ace/sandbox/marrs/org.apache.ace.client.repository.impl/src/org/apache/ace/client/repository/stateful/impl/StatefulTargetRepositoryImpl.java
URL: http://svn.apache.org/viewvc/ace/sandbox/marrs/org.apache.ace.client.repository.impl/src/org/apache/ace/client/repository/stateful/impl/StatefulTargetRepositoryImpl.java?rev=1357570&view=auto
==============================================================================
--- ace/sandbox/marrs/org.apache.ace.client.repository.impl/src/org/apache/ace/client/repository/stateful/impl/StatefulTargetRepositoryImpl.java (added)
+++ ace/sandbox/marrs/org.apache.ace.client.repository.impl/src/org/apache/ace/client/repository/stateful/impl/StatefulTargetRepositoryImpl.java Thu Jul  5 12:09:30 2012
@@ -0,0 +1,757 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.ace.client.repository.stateful.impl;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Dictionary;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Properties;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.apache.ace.client.repository.RepositoryAdmin;
+import org.apache.ace.client.repository.RepositoryObject;
+import org.apache.ace.client.repository.RepositoryUtil;
+import org.apache.ace.client.repository.SessionFactory;
+import org.apache.ace.client.repository.helper.bundle.BundleHelper;
+import org.apache.ace.client.repository.object.ArtifactObject;
+import org.apache.ace.client.repository.object.DeploymentArtifact;
+import org.apache.ace.client.repository.object.DeploymentVersionObject;
+import org.apache.ace.client.repository.object.TargetObject;
+import org.apache.ace.client.repository.object.FeatureObject;
+import org.apache.ace.client.repository.object.DistributionObject;
+import org.apache.ace.client.repository.repository.ArtifactRepository;
+import org.apache.ace.client.repository.repository.DeploymentVersionRepository;
+import org.apache.ace.client.repository.repository.TargetRepository;
+import org.apache.ace.client.repository.stateful.StatefulTargetObject;
+import org.apache.ace.client.repository.stateful.StatefulTargetRepository;
+import org.apache.ace.log.LogDescriptor;
+import org.apache.ace.log.LogEvent;
+import org.apache.ace.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 StatefulTargetRepository. If an <code>AuditLogStore</code> is present,
+ * it will be used; it is assumed that the auditlog store is up to date.
+ */
+public class StatefulTargetRepositoryImpl implements StatefulTargetRepository, EventHandler {
+    private BundleContext m_context; /* Injected by dependency manager */
+    private ArtifactRepository m_artifactRepository; /* Injected by dependency manager */
+    private TargetRepository m_targetRepository; /* 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, StatefulTargetObjectImpl> m_repository = new ConcurrentHashMap<String, StatefulTargetObjectImpl>();
+    private Map<String, StatefulTargetObjectImpl> m_index = new ConcurrentHashMap<String, StatefulTargetObjectImpl>();
+    private final String m_sessionID;
+
+    public StatefulTargetRepositoryImpl(String sessionID) {
+        m_sessionID = sessionID;
+    }
+
+    public StatefulTargetObject create(Map<String, String> attributes, Map<String, String> tags)
+        throws IllegalArgumentException {
+        throw new UnsupportedOperationException("Creating StatefulTargetObjects is not supported.");
+    }
+
+    public List<StatefulTargetObject> get() {
+        synchronized (m_repository) {
+            List<StatefulTargetObject> result = new ArrayList<StatefulTargetObject>();
+            for (StatefulTargetObjectImpl sgoi : m_repository.values()) {
+                result.add(sgoi);
+            }
+            return result;
+        }
+    }
+
+    public List<StatefulTargetObject> get(Filter filter) {
+        synchronized (m_repository) {
+            List<StatefulTargetObject> result = new ArrayList<StatefulTargetObject>();
+            for (StatefulTargetObject entry : m_repository.values()) {
+                if (filter.match(entry.getDictionary())) {
+                    result.add(entry);
+                }
+            }
+            return result;
+        }
+    }
+
+    public StatefulTargetObject get(String definition) {
+        return m_index.get(definition);
+    }
+
+    public void remove(StatefulTargetObject entity) {
+        synchronized (m_repository) {
+            StatefulTargetObjectImpl statefulTarget = (StatefulTargetObjectImpl) entity;
+            unregister(statefulTarget.getID());
+            removeStateful(statefulTarget);
+            // Ensure the external side sees the changes we've made...
+            statefulTarget.updateTargetObject(false);
+        }
+    }
+
+    public StatefulTargetObject preregister(Map<String, String> attributes, Map<String, String> tags) {
+        synchronized (m_repository) {
+            TargetObject to = m_targetRepository.create(attributes, tags);
+            return createStateful(to.getID());
+        }
+    }
+
+    public void unregister(String targetID) {
+        synchronized (m_repository) {
+            TargetObject to = getTargetObject(targetID);
+            if (to == null) {
+                throw new IllegalArgumentException(targetID + " does not represent a TargetObject.");
+            }
+            else {
+                m_targetRepository.remove(to);
+                // No need to inform the stateful representation; this will be done by the event handler.
+            }
+        }
+    }
+
+    public void refresh() {
+        populate();
+    }
+
+    /**
+     * Gets the <code>TargetObject</code> which is identified by the <code>targetID</code>.
+     * 
+     * @param targetID A string representing a target ID.
+     * @return The <code>TargetObject</code> from the <code>TargetRepository</code> which has the given
+     *         ID, or <code>null</code> if none can be found.
+     */
+    TargetObject getTargetObject(String targetID) {
+// synchronized(m_repository) {
+        try {
+            List<TargetObject> targets =
+                m_targetRepository.get(m_context.createFilter("(" + TargetObject.KEY_ID + "="
+                    + RepositoryUtil.escapeFilterValue(targetID) + ")"));
+            if ((targets != null) && (targets.size() == 1)) {
+                return targets.get(0);
+            }
+            else {
+                return null;
+            }
+        }
+        catch (InvalidSyntaxException e) {
+            // The filter syntax is illegal, probably a bad target ID.
+            return null;
+        }
+// }
+    }
+
+    /**
+     * Gets the stateful representation of the given target ID.
+     * 
+     * @param targetID A string representing a target ID.
+     * @return The <code>StatefulTargetyObjectImpl</code> which handles the given ID,
+     *         or <code>null</code> if none can be found.
+     */
+    StatefulTargetObjectImpl getStatefulTargetObject(String targetID) {
+        synchronized (m_repository) {
+            return m_repository.get(targetID);
+        }
+    }
+
+    /**
+     * Creates and registers a new stateful target object based on the given ID.
+     * 
+     * @param targetID A string representing a target ID.
+     * @return The newly created and registered <code>StatefulTargetObjectImpl</code>.
+     */
+    private StatefulTargetObjectImpl createStateful(String targetID) {
+        synchronized (m_repository) {
+            StatefulTargetObjectImpl result = new StatefulTargetObjectImpl(this, targetID);
+            if (add(result)) {
+                return result;
+            }
+            else {
+                throw new IllegalArgumentException("The StateTargetObject " + targetID + " already exists.");
+            }
+        }
+    }
+
+    /**
+     * Removes the given entity from this object's repository, and notifies
+     * interested parties of this.
+     * 
+     * @param entity The StatefulTargetObjectImpl to be removed.
+     */
+    void removeStateful(StatefulTargetObjectImpl entity) {
+        synchronized (m_repository) {
+            m_repository.remove(entity.getID());
+            notifyChanged(entity, StatefulTargetObject.TOPIC_REMOVED);
+        }
+    }
+
+    /**
+     * Adds the given stateful object to this object's repository, and notifies
+     * interested parties of this change.
+     * 
+     * @param stoi A <code>StatefulTargetObjectImpl</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(StatefulTargetObjectImpl stoi) {
+        if (!m_repository.containsKey(stoi)) {
+            m_repository.put(stoi.getID(), stoi);
+            m_index.put(stoi.getDefinition(), stoi);
+            notifyChanged(stoi, StatefulTargetObject.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 target ID.
+     * 
+     * @param targetID A string representing a target ID.
+     * @return a list of <code>AuditEvent</code>s related to this target ID,
+     *         ordered in the order they happened. If no events can be found, and empty list will be returned.
+     */
+    List<LogEvent> getAuditEvents(String targetID) {
+        return getAuditEvents(getAllDescriptors(targetID));
+    }
+
+    /**
+     * Gets all auditlog descriptors which are related to a given target.
+     * 
+     * @param targetID The target ID
+     * @return A list of LogDescriptors, in no particular order.
+     */
+    List<LogDescriptor> getAllDescriptors(String targetID) {
+        List<LogDescriptor> result = new ArrayList<LogDescriptor>();
+        try {
+            List<LogDescriptor> descriptors = m_auditLogStore.getDescriptors(targetID);
+            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 target 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 target 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.getTargetID().equals(d.getTargetID()))) {
+                    diffs = new LogDescriptor(s.getTargetID(), s.getLogID(), d.getRangeSet().diffDest(s.getRangeSet()));
+                }
+            }
+            descriptors.add(diffs);
+        }
+        return descriptors;
+    }
+
+    /**
+     * See {@link DeploymentRepository#getDeploymentVersion(java.lang.String)}.
+     */
+    DeploymentVersionObject getMostRecentDeploymentVersion(String targetID) {
+        return m_deploymentRepository.getMostRecentDeploymentVersion(targetID);
+    }
+
+    /**
+     * Based on the information in this stateful object, creates a <code>TargetObject</code>
+     * in the <code>TargetRepository</code>.
+     * This function is intended to be used for targets which are not yet represented
+     * in the <code>TargetRepository</code>; if they already are, an <code>IllegalArgumentException</code>
+     * will be thrown.
+     * 
+     * @param targetID A string representing the ID of the new target.
+     */
+    void register(String targetID) {
+        Map<String, String> attr = new HashMap<String, String>();
+        attr.put(TargetObject.KEY_ID, targetID);
+        Map<String, String> tags = new HashMap<String, String>();
+        m_targetRepository.create(attr, tags);
+        getStatefulTargetObject(targetID).updateTargetObject(false);
+    }
+
+    /**
+     * Notifies interested parties of a change to a <code>StatefulTargetObject</code>.
+     * 
+     * @param stoi The <code>StatefulTargetObject</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(StatefulTargetObject stoi, String topic, Properties additionalProperties) {
+        additionalProperties.put(RepositoryObject.EVENT_ENTITY, stoi);
+        additionalProperties.put(SessionFactory.SERVICE_SID, m_sessionID);
+        m_eventAdmin.postEvent(new Event(topic, (Dictionary) additionalProperties));
+    }
+
+    /**
+     * Notifies interested parties of a change to a <code>StatefulTargetObject</code>.
+     * 
+     * @param stoi The <code>StatefulTargetObject</code> which has changed.
+     * @param topic A topic string for posting the event.
+     */
+    void notifyChanged(StatefulTargetObject stoi, String topic) {
+        notifyChanged(stoi, topic, new Properties());
+    }
+
+    /**
+     * Reads the information sources to generate the stateful objects.
+     */
+    private void populate() {
+        synchronized (m_repository) {
+            List<StatefulTargetObjectImpl> touched = new ArrayList<StatefulTargetObjectImpl>();
+            touched.addAll(parseTargetRepository());
+            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<StatefulTargetObjectImpl> all = new ArrayList<StatefulTargetObjectImpl>(m_repository.values());
+            all.removeAll(touched);
+            for (StatefulTargetObjectImpl stoi : all) {
+                stoi.updateTargetObject(false);
+                stoi.updateDeploymentVersions(null);
+                stoi.updateAuditEvents(true);
+            }
+            // Furthermore, for all those we _did_ see, we need to make sure their deployment versions
+            // are up to date.
+            for (StatefulTargetObjectImpl stoi : touched) {
+                stoi.updateDeploymentVersions(null);
+                stoi.updateTargetObject(true);
+            }
+        }
+    }
+
+    /**
+     * Checks all inhabitants of the <code>TargetRepository</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 target objects that have been touched by this action.
+     */
+    private List<StatefulTargetObjectImpl> parseTargetRepository() {
+        List<StatefulTargetObjectImpl> result = new ArrayList<StatefulTargetObjectImpl>();
+        for (TargetObject to : m_targetRepository.get()) {
+            StatefulTargetObjectImpl stoi = getStatefulTargetObject(to.getID());
+            if (stoi == null) {
+                result.add(createStateful(to.getID()));
+            }
+            else {
+                result.add(stoi);
+                stoi.updateTargetObject(false);
+            }
+        }
+        return result;
+    }
+
+    /**
+     * Checks the audit log to see whether we already have a
+     * stateful object for all targets mentioned there.
+     * 
+     * @param needsVerify states whether the objects which are 'touched' by this
+     *        actions should verify their existence.
+     */
+    private List<StatefulTargetObjectImpl> parseAuditLog() {
+        List<StatefulTargetObjectImpl> result = new ArrayList<StatefulTargetObjectImpl>();
+        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 log descriptors.
+            return result;
+        }
+
+        Set<String> targetIDs = new HashSet<String>();
+        for (LogDescriptor l : descriptors) {
+            targetIDs.add(l.getTargetID());
+        }
+
+        /*
+         * 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 targets which have more than one log.
+         */
+        synchronized (m_repository) {
+            for (String targetID : targetIDs) {
+                StatefulTargetObjectImpl stoi = getStatefulTargetObject(targetID);
+                if (stoi == null) {
+                    result.add(createStateful(targetID));
+                }
+                else {
+                    result.add(stoi);
+                    stoi.updateAuditEvents(false);
+                }
+            }
+        }
+        return result;
+    }
+
+    /**
+     * Approves the changes that will happen to the target based on the
+     * changes in the shop by generating a new deployment version.
+     * 
+     * @param targetID A string representing a target ID.
+     * @return The version identifier of the new deployment package.
+     * @throws java.io.IOException When there is a problem generating the deployment version.
+     */
+    String approve(String targetID) throws IOException {
+        return generateDeploymentVersion(targetID).getVersion();
+    }
+
+    /**
+     * Generates an array of bundle URLs which have to be deployed on
+     * the target, 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 distribution- and
+     * feature objects?
+     * 
+     * @param targetID A string representing a target.
+     * @return An array of artifact URLs.
+     * @throws java.io.IOException When there is a problem processing an artifact for deployment.
+     */
+    DeploymentArtifact[] getNecessaryDeploymentArtifacts(String targetID, String version) throws IOException {
+        TargetObject to = getTargetObject(targetID);
+
+        Map<ArtifactObject, String> bundles = new HashMap<ArtifactObject, String>();
+        Map<ArtifactObject, String> artifacts = new HashMap<ArtifactObject, String>();
+        Map<ArtifactObject, Map<FeatureObject, List<DistributionObject>>> path =
+            new HashMap<ArtifactObject, Map<FeatureObject, List<DistributionObject>>>();
+
+        // First, find all basic bundles and artifacts. An while we're traversing the
+        // tree of objects, build the tree of properties.
+        if (to != null) {
+            for (DistributionObject distribution : to.getDistributions()) {
+                for (FeatureObject feature : distribution.getFeatures()) {
+                    for (ArtifactObject artifact : feature.getArtifacts()) {
+                        if (m_bundleHelper.canUse(artifact)) {
+                            bundles.put(artifact, m_bundleHelper.getResourceProcessorPIDs(artifact));
+                        }
+                        else {
+                            artifacts.put(artifact, artifact.getProcessorPID());
+                        }
+                        Map<FeatureObject, List<DistributionObject>> featureToDistribution = path.get(artifact);
+                        if (featureToDistribution == null) {
+                            featureToDistribution = new HashMap<FeatureObject, List<DistributionObject>>();
+                            path.put(artifact, featureToDistribution);
+                        }
+                        List<DistributionObject> distributions = featureToDistribution.get(feature);
+                        if (distributions == null) {
+                            distributions = new ArrayList<DistributionObject>();
+                            featureToDistribution.put(feature, distributions);
+                        }
+                        distributions.add(distribution);
+                    }
+                }
+            }
+        }
+
+        // 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());
+
+            String repositoryPath = getRepositoryPath(bundle, path);
+            if (repositoryPath != null) {
+                directives.put(DeploymentArtifact.REPOSITORY_PATH, repositoryPath);
+            }
+
+            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());
+            if (artifact.getResourceId() != null) {
+                directives.put(DeploymentArtifact.DIRECTIVE_KEY_RESOURCE_ID, artifact.getResourceId());
+            }
+
+            String repositoryPath = getRepositoryPath(artifact, path);
+            if (repositoryPath != null) {
+                directives.put(DeploymentArtifact.REPOSITORY_PATH, repositoryPath);
+            }
+            result.add(m_deploymentRepository.createDeploymentArtifact(
+                m_artifactRepository.preprocessArtifact(artifact, to, targetID, version), directives));
+        }
+
+        return result.toArray(new DeploymentArtifact[result.size()]);
+    }
+
+    private String getRepositoryPath(ArtifactObject artifact,
+        Map<ArtifactObject, Map<FeatureObject, List<DistributionObject>>> path) {
+        StringBuilder builder = new StringBuilder();
+        Map<FeatureObject, List<DistributionObject>> featureToDistribution = path.get(artifact);
+        if (featureToDistribution != null) {
+            for (Entry<FeatureObject, List<DistributionObject>> entry : featureToDistribution.entrySet()) {
+                for (DistributionObject distribution : entry.getValue()) {
+                    builder.append(entry.getKey().getName()).append(';').append(distribution.getName()).append(',');
+                }
+            }
+        }
+        else {
+            return null;
+        }
+        builder.setLength(builder.length() - 1);
+        return builder.toString();
+    }
+
+    /**
+     * Quick method to find all artifacts that need to be deployed to a target.
+     */
+    ArtifactObject[] getNecessaryArtifacts(String targetID) {
+        List<ArtifactObject> result = new ArrayList<ArtifactObject>();
+        TargetObject to = getTargetObject(targetID);
+
+        Map<String, ArtifactObject> allProcessors = new HashMap<String, ArtifactObject>();
+        for (ArtifactObject bundle : m_artifactRepository.getResourceProcessors()) {
+            allProcessors.put(m_bundleHelper.getResourceProcessorPIDs(bundle), bundle);
+        }
+
+        if (to != null) {
+            for (DistributionObject distribution : to.getDistributions()) {
+                for (FeatureObject feature : distribution.getFeatures()) {
+                    for (ArtifactObject artifact : feature.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 target,
+     * based on the artifacts it is linked to by the distributions it is
+     * associated to.
+     * 
+     * @param targetID A string representing a target.
+     * @return A new DeploymentVersionObject, representing this new version for the target.
+     * @throws java.io.IOException When there is a problem determining the artifacts to be deployed.
+     */
+    DeploymentVersionObject generateDeploymentVersion(String targetID) throws IOException {
+        Map<String, String> attr = new HashMap<String, String>();
+        attr.put(DeploymentVersionObject.KEY_TARGETID, targetID);
+        Map<String, String> tags = new HashMap<String, String>();
+
+        DeploymentVersionObject mostRecentDeploymentVersion = getMostRecentDeploymentVersion(targetID);
+        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(targetID, nextVersion));
+
+            StatefulTargetObjectImpl stoi = getStatefulTargetObject(targetID);
+            if (stoi == null) {
+                createStateful(targetID);
+            }
+            else {
+                stoi.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) {
+        String topic = event.getTopic();
+        if (TargetObject.TOPIC_ADDED.equals(topic)) {
+            synchronized (m_repository) {
+                String id = ((TargetObject) event.getProperty(RepositoryObject.EVENT_ENTITY)).getID();
+                StatefulTargetObjectImpl stoi = getStatefulTargetObject(id);
+                if (stoi == null) {
+                    createStateful(id);
+                }
+                else {
+                    stoi.updateTargetObject(true);
+                }
+            }
+        }
+        else if (TargetObject.TOPIC_REMOVED.equals(topic)) {
+            synchronized (m_repository) {
+                String id = ((TargetObject) event.getProperty(RepositoryObject.EVENT_ENTITY)).getID();
+                StatefulTargetObjectImpl stoi = getStatefulTargetObject(id);
+                // if the stateful target is already gone; we don't have to do anything...
+                if (stoi != null) {
+                    stoi.updateTargetObject(true);
+                }
+            }
+        }
+        else if (DeploymentVersionObject.TOPIC_ADDED.equals(topic) || DeploymentVersionObject.TOPIC_REMOVED.equals(topic)) {
+            synchronized (m_repository) {
+                DeploymentVersionObject deploymentVersionObject = ((DeploymentVersionObject) event.getProperty(RepositoryObject.EVENT_ENTITY));
+                String id = deploymentVersionObject.getTargetID();
+                StatefulTargetObjectImpl stoi = getStatefulTargetObject(id);
+                if (stoi == null) {
+                    createStateful(id);
+                }
+                else {
+                    stoi.updateDeploymentVersions(deploymentVersionObject);
+                }
+            }
+        }
+        else if (RepositoryAdmin.TOPIC_LOGIN.equals(topic) || RepositoryAdmin.TOPIC_REFRESH.equals(topic)) {
+            synchronized (m_repository) {
+                populate();
+            }
+        }
+        else {
+            // Something else has changed; however, the entire shop may have an influence on
+            // any target, so recheck everything.
+            synchronized (m_repository) {
+                for (StatefulTargetObjectImpl stoi : m_repository.values()) {
+                    stoi.determineStatus();
+                }
+            }
+        }
+    }
+
+    boolean needsNewVersion(ArtifactObject artifact, String targetID, String version) {
+        return m_artifactRepository.needsNewVersion(artifact, getTargetObject(targetID), targetID, version);
+    }
+}
\ No newline at end of file

Added: ace/sandbox/marrs/org.apache.ace.client.repository.impl/test/org/apache/ace/client/repository/impl/AdminTestUtil.java
URL: http://svn.apache.org/viewvc/ace/sandbox/marrs/org.apache.ace.client.repository.impl/test/org/apache/ace/client/repository/impl/AdminTestUtil.java?rev=1357570&view=auto
==============================================================================
--- ace/sandbox/marrs/org.apache.ace.client.repository.impl/test/org/apache/ace/client/repository/impl/AdminTestUtil.java (added)
+++ ace/sandbox/marrs/org.apache.ace.client.repository.impl/test/org/apache/ace/client/repository/impl/AdminTestUtil.java Thu Jul  5 12:09:30 2012
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.ace.client.repository.impl;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+public class AdminTestUtil {
+
+    public static byte[] copy(InputStream in) throws IOException {
+        byte[] result = new byte[in.available()];
+        in.read(result);
+        return result;
+    }
+
+    public static boolean byteArraysEqual(byte[] left, byte[] right) {
+        if (left.length != right.length) {
+            return false;
+        }
+        for (int i = 0; i < right.length; i++) {
+            if (left[i] != right[i]) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    public static byte[] copy(byte[] input) {
+        byte[] result = new byte[input.length];
+        for (int i = 0; i < input.length; i++) {
+            result[i] = input[i];
+        }
+        return result;
+    }
+}

Added: ace/sandbox/marrs/org.apache.ace.client.repository.impl/test/org/apache/ace/client/repository/impl/ArtifactTest.java
URL: http://svn.apache.org/viewvc/ace/sandbox/marrs/org.apache.ace.client.repository.impl/test/org/apache/ace/client/repository/impl/ArtifactTest.java?rev=1357570&view=auto
==============================================================================
--- ace/sandbox/marrs/org.apache.ace.client.repository.impl/test/org/apache/ace/client/repository/impl/ArtifactTest.java (added)
+++ ace/sandbox/marrs/org.apache.ace.client.repository.impl/test/org/apache/ace/client/repository/impl/ArtifactTest.java Thu Jul  5 12:09:30 2012
@@ -0,0 +1,178 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.ace.client.repository.impl;
+
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.ace.client.repository.helper.ArtifactHelper;
+import org.apache.ace.client.repository.helper.ArtifactPreprocessor;
+import org.apache.ace.client.repository.helper.bundle.BundleHelper;
+import org.apache.ace.client.repository.helper.bundle.impl.BundleHelperImpl;
+import org.apache.ace.client.repository.object.ArtifactObject;
+import org.apache.ace.test.utils.TestUtils;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.Filter;
+import org.osgi.framework.FrameworkUtil;
+import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.service.log.LogService;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+/**
+ * Tests the behavior of the ArtifactObject class, most prominently, checking whether
+ * delegation to the Helpers is done at the right moments.
+ */
+public class ArtifactTest {
+
+    private ArtifactRepositoryImpl m_artifactRepository;
+
+    @BeforeMethod(alwaysRun = true)
+    public void init() {
+        BundleContext bc = TestUtils.createMockObjectAdapter(BundleContext.class, new Object() {
+            @SuppressWarnings("unused")
+            public Filter createFilter(String filter) throws InvalidSyntaxException {
+                return FrameworkUtil.createFilter(filter);
+            }
+        });
+
+        m_artifactRepository = new ArtifactRepositoryImpl(TestUtils.createNullObject(ChangeNotifier.class));
+        TestUtils.configureObject(m_artifactRepository, LogService.class);
+        TestUtils.configureObject(m_artifactRepository, BundleContext.class, bc);
+    }
+
+    @Test( groups = { TestUtils.UNIT } )
+    public void testAttributeChecking() {
+        ArtifactHelper helper = new MockHelper("yourURL");
+
+        try {
+            createArtifact("myMime", "myUrl", null, null);
+            assert false : "There is no helper for this type of artifact.";
+        }
+        catch (IllegalArgumentException iae) {
+            // expected
+        }
+
+        m_artifactRepository.addHelper("myMime", helper);
+
+        ArtifactObject obj = createArtifact("myMime", "myUrl", null, null);
+
+        assert obj.getURL().equals("yourURL");
+
+        try {
+            m_artifactRepository.getHelper("yourMime");
+            assert false : "We have not registered this helper.";
+        }
+        catch (IllegalArgumentException iae) {
+            // expected
+        }
+    }
+
+    @Test( groups = { TestUtils.UNIT } )
+    public void testResourceProcessorFiltering() throws InvalidSyntaxException {
+        m_artifactRepository.addHelper("myMime", new MockHelper());
+        m_artifactRepository.addHelper(BundleHelper.MIMETYPE, new BundleHelperImpl());
+
+        createArtifact(BundleHelper.MIMETYPE, "normalBundle", "normalBundle", null);
+
+        ArtifactObject resourceProcessor1 = createArtifact(BundleHelper.MIMETYPE, "resourceProcessor1", "resourceProcessor1", "somePID");
+        ArtifactObject resourceProcessor2 = createArtifact(BundleHelper.MIMETYPE, "resourceProcessor2", "resourceProcessor2", "someOtherPID");
+
+        ArtifactObject myArtifact = createArtifact("myMime", "myArtifact", null, null);
+
+        assert m_artifactRepository.get().size() == 2 : "We expect to find two artifacts, but we find " + m_artifactRepository.get().size();
+
+        List<ArtifactObject> list = m_artifactRepository.get(m_artifactRepository.createFilter("(!(" + BundleHelper.KEY_SYMBOLICNAME + "=normalBundle))"));
+        assert (list.size() == 1) && list.contains(myArtifact) : "We expect to find one artifact when filtering, but we find " + list.size();
+
+        list = m_artifactRepository.getResourceProcessors();
+        assert (list.size() == 2) && list.contains(resourceProcessor1) && list.contains(resourceProcessor2) : "We expect to find both our resource processors when asking for them; we find " + list.size() + " artifacts.";
+
+       m_artifactRepository.get(m_artifactRepository.createFilter("(" + BundleHelper.MIMETYPE + "=my\\(Mi\\*me)"));
+    }
+
+    private ArtifactObject createArtifact(String mimetype, String URL, String symbolicName, String processorPID) {
+        Map<String, String> attributes = new HashMap<String, String>();
+        attributes.put(ArtifactObject.KEY_MIMETYPE, mimetype);
+        attributes.put(ArtifactObject.KEY_URL, URL);
+        Map<String, String> tags = new HashMap<String, String>();
+
+        if (symbolicName != null) {
+            attributes.put(BundleHelper.KEY_SYMBOLICNAME, symbolicName);
+        }
+        if (processorPID != null) {
+            attributes.put(BundleHelper.KEY_RESOURCE_PROCESSOR_PID, processorPID);
+        }
+
+        return m_artifactRepository.create(attributes, tags);
+    }
+}
+
+/**
+ * Helper for testing the ArtifactObject. In the constructor, a <code>replaceURL</code> can
+ * be passed in to test the attribute normalization.
+ */
+class MockHelper implements ArtifactHelper {
+    private final String m_replaceURL;
+
+    MockHelper() {
+        this(null);
+    }
+
+    MockHelper(String replaceURL) {
+        m_replaceURL = replaceURL;
+    }
+
+    public Map<String, String> checkAttributes(Map<String, String> attributes) {
+        if ((m_replaceURL != null) && attributes.containsKey(ArtifactObject.KEY_URL)) {
+            attributes.put(ArtifactObject.KEY_URL, m_replaceURL);
+        }
+        return attributes;
+    }
+
+    public Comparator<ArtifactObject> getComparator() {
+        return null;
+    }
+
+    public String[] getDefiningKeys() {
+        return new String[]{ArtifactObject.KEY_URL};
+    }
+
+    public String[] getMandatoryAttributes() {
+        return new String[]{ArtifactObject.KEY_URL};
+    }
+
+    public boolean canUse(ArtifactObject object) {
+        return false;
+    }
+
+    public <TYPE extends ArtifactObject> String getAssociationFilter(TYPE obj, Map<String, String> properties) {
+        return null;
+    }
+
+    public <TYPE extends ArtifactObject> int getCardinality(TYPE obj, Map<String, String> properties) {
+        return 0;
+    }
+
+    public ArtifactPreprocessor getPreprocessor() {
+        return null;
+    }
+}

Added: ace/sandbox/marrs/org.apache.ace.client.repository.impl/test/org/apache/ace/client/repository/impl/CachedRepositoryImplTest.java
URL: http://svn.apache.org/viewvc/ace/sandbox/marrs/org.apache.ace.client.repository.impl/test/org/apache/ace/client/repository/impl/CachedRepositoryImplTest.java?rev=1357570&view=auto
==============================================================================
--- ace/sandbox/marrs/org.apache.ace.client.repository.impl/test/org/apache/ace/client/repository/impl/CachedRepositoryImplTest.java (added)
+++ ace/sandbox/marrs/org.apache.ace.client.repository.impl/test/org/apache/ace/client/repository/impl/CachedRepositoryImplTest.java Thu Jul  5 12:09:30 2012
@@ -0,0 +1,124 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.ace.client.repository.impl;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+import org.apache.ace.repository.Repository;
+import org.apache.ace.repository.ext.BackupRepository;
+import org.apache.ace.repository.ext.CachedRepository;
+import org.apache.ace.repository.ext.impl.CachedRepositoryImpl;
+import org.apache.ace.test.utils.TestUtils;
+import org.testng.annotations.Test;
+
+public class CachedRepositoryImplTest {
+
+    /**
+     * Initial checkout: the remote repository contains some data for a given version,
+     * we make the cached repository do a checkout, and check whether all data arrives at the
+     * right places: in getLocal, and in the backup repository.
+     */
+    @Test( groups = { TestUtils.UNIT } )
+    public void testInitialCheckout() throws IllegalArgumentException, IOException {
+        Repository m_repository = new MockRepository();
+        byte[] testContent = new byte[] {'i', 'n', 'i', 't', 'i', 'a', 'l'};
+        m_repository.commit(new ByteArrayInputStream(testContent), 0);
+        BackupRepository m_backupRepository = new MockBackupRepository();
+
+        CachedRepository m_cachedRepository = new CachedRepositoryImpl(m_repository, m_backupRepository, 0);
+
+        InputStream input = m_cachedRepository.checkout(1);
+        byte[] inputBytes = AdminTestUtil.copy(input);
+        assert AdminTestUtil.byteArraysEqual(inputBytes, testContent) : "We got something different than 'initial' from checkout: " + new String(inputBytes);
+        input = m_cachedRepository.getLocal(false);
+        inputBytes = AdminTestUtil.copy(input);
+        assert AdminTestUtil.byteArraysEqual(inputBytes, testContent) : "We got something different than 'initial' from getLocal: " + new String(inputBytes);
+        input = m_backupRepository.read();
+        inputBytes = AdminTestUtil.copy(input);
+        assert AdminTestUtil.byteArraysEqual(inputBytes, testContent) : "We got something different than 'initial' from the backup repository: " + new String(inputBytes);
+    }
+
+    /**
+     * There are two types of commit, one that takes an input stream, and one that
+     * simply flushes whatever is in the current to the remote repository.
+     * After each commit, we should be able to renew the cached repository,
+     * and checkout the data we put in before.
+     */
+    @Test( groups = { TestUtils.UNIT } )
+    public void testCommit() throws IllegalArgumentException, IOException {
+        Repository m_repository = new MockRepository();
+        BackupRepository m_backupRepository = new MockBackupRepository();
+
+        CachedRepository m_cachedRepository = new CachedRepositoryImpl(m_repository, m_backupRepository, 0);
+        byte[] testContent = new byte[] {'i', 'n', 'i', 't', 'i', 'a', 'l'};
+
+        InputStream input = new ByteArrayInputStream(testContent);
+        m_cachedRepository.commit(input, 0);
+
+        m_cachedRepository = new CachedRepositoryImpl(m_repository, m_backupRepository, 0);
+        input = m_cachedRepository.checkout(1);
+        byte[] inputBytes = AdminTestUtil.copy(input);
+        assert AdminTestUtil.byteArraysEqual(inputBytes, testContent) : "We got something different than 'initial' from checkout: " + new String(inputBytes);
+
+        byte[] newTestContent = new byte[] {'n', 'e', 'w'};
+
+        m_cachedRepository.writeLocal(new ByteArrayInputStream(newTestContent));
+
+        m_cachedRepository.commit();
+
+        m_cachedRepository = new CachedRepositoryImpl(m_repository, m_backupRepository, 0);
+        input = m_cachedRepository.checkout(2);
+        inputBytes = AdminTestUtil.copy(input);
+        assert AdminTestUtil.byteArraysEqual(inputBytes, newTestContent) : "We got something different than 'new' from checkout: " + new String(inputBytes);
+    }
+
+    /**
+     * After checking out and changing stuff, we want to revert to the old version.
+     * @throws IOException
+     * @throws IllegalArgumentException
+     */
+    @Test( groups = { TestUtils.UNIT } )
+    public void testRevert() throws IllegalArgumentException, IOException {
+        Repository m_repository = new MockRepository();
+        BackupRepository m_backupRepository = new MockBackupRepository();
+
+        CachedRepository m_cachedRepository = new CachedRepositoryImpl(m_repository, m_backupRepository, 0);
+        byte[] testContent = new byte[] {'i', 'n', 'i', 't', 'i', 'a', 'l'};
+
+        InputStream input = new ByteArrayInputStream(testContent);
+        m_cachedRepository.commit(input, 0);
+
+        m_cachedRepository = new CachedRepositoryImpl(m_repository, m_backupRepository, 0);
+        input = m_cachedRepository.checkout(1);
+
+        byte[] newTestContent = new byte[] {'n', 'e', 'w'};
+
+        m_cachedRepository.writeLocal(new ByteArrayInputStream(newTestContent));
+        input = m_cachedRepository.getLocal(false);
+        byte[] inputBytes = AdminTestUtil.copy(input);
+        assert AdminTestUtil.byteArraysEqual(inputBytes, newTestContent) : "We got something different than 'new' from getLocal: " + new String(inputBytes);
+
+        m_cachedRepository.revert();
+        input = m_cachedRepository.getLocal(false);
+        inputBytes = AdminTestUtil.copy(input);
+        assert AdminTestUtil.byteArraysEqual(inputBytes, testContent) : "We got something different than 'initial' from getLocal: " + new String(inputBytes);
+    }
+}

Added: ace/sandbox/marrs/org.apache.ace.client.repository.impl/test/org/apache/ace/client/repository/impl/FilebasedBackupRepositoryTest.java
URL: http://svn.apache.org/viewvc/ace/sandbox/marrs/org.apache.ace.client.repository.impl/test/org/apache/ace/client/repository/impl/FilebasedBackupRepositoryTest.java?rev=1357570&view=auto
==============================================================================
--- ace/sandbox/marrs/org.apache.ace.client.repository.impl/test/org/apache/ace/client/repository/impl/FilebasedBackupRepositoryTest.java (added)
+++ ace/sandbox/marrs/org.apache.ace.client.repository.impl/test/org/apache/ace/client/repository/impl/FilebasedBackupRepositoryTest.java Thu Jul  5 12:09:30 2012
@@ -0,0 +1,75 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.ace.client.repository.impl;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+
+import org.apache.ace.repository.ext.impl.FilebasedBackupRepository;
+import org.apache.ace.test.utils.TestUtils;
+import org.testng.annotations.Test;
+
+public class FilebasedBackupRepositoryTest {
+
+    /**
+     * A basic scenario: we write, backup, write again, and revert.
+     */
+    @Test( groups = { TestUtils.UNIT } )
+    public void testFilebasedBackupRepository() throws IOException {
+        File current = File.createTempFile("testFilebasedBackupRepository", null);
+        File backup = File.createTempFile("testFilebasedBackupRepository", null);
+        current.deleteOnExit();
+        backup.deleteOnExit();
+
+        FilebasedBackupRepository rep = new FilebasedBackupRepository(current, backup);
+
+        byte[] testContent = new byte[] {'i', 'n', 'i', 't', 'i', 'a', 'l'};
+
+        // write initial content
+        rep.write(new ByteArrayInputStream(testContent));
+
+        // read initial content
+        InputStream input = rep.read();
+        byte[] inputBytes = AdminTestUtil.copy(input);
+        assert AdminTestUtil.byteArraysEqual(inputBytes, testContent) : "We got something different than 'initial' from read: " + new String(inputBytes);
+
+        // backup what's in the repository
+        rep.backup();
+
+        // write new content
+        byte[] newTestContent = new byte[] {'n', 'e', 'w'};
+        rep.write(new ByteArrayInputStream(newTestContent));
+
+        // read current content
+        input = rep.read();
+        inputBytes = AdminTestUtil.copy(input);
+        assert AdminTestUtil.byteArraysEqual(inputBytes, newTestContent) : "We got something different than 'new' from read: " + new String(inputBytes);
+
+        // revert to previous (initial) content
+        rep.restore();
+
+        // read current content
+        input = rep.read();
+        inputBytes = AdminTestUtil.copy(input);
+        assert AdminTestUtil.byteArraysEqual(inputBytes, testContent) : "We got something different than 'initial' from read: " + new String(inputBytes);
+    }
+
+}

Added: ace/sandbox/marrs/org.apache.ace.client.repository.impl/test/org/apache/ace/client/repository/impl/MockBackupRepository.java
URL: http://svn.apache.org/viewvc/ace/sandbox/marrs/org.apache.ace.client.repository.impl/test/org/apache/ace/client/repository/impl/MockBackupRepository.java?rev=1357570&view=auto
==============================================================================
--- ace/sandbox/marrs/org.apache.ace.client.repository.impl/test/org/apache/ace/client/repository/impl/MockBackupRepository.java (added)
+++ ace/sandbox/marrs/org.apache.ace.client.repository.impl/test/org/apache/ace/client/repository/impl/MockBackupRepository.java Thu Jul  5 12:09:30 2012
@@ -0,0 +1,55 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.ace.client.repository.impl;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+import org.apache.ace.repository.ext.BackupRepository;
+
+
+public class MockBackupRepository implements BackupRepository {
+    private byte[] m_current;
+    private byte[] m_backup;
+
+    public boolean backup() throws IOException {
+        if (m_current == null) {
+            return false;
+        }
+        m_backup = AdminTestUtil.copy(m_current);
+        return true;
+    }
+
+    public InputStream read() throws IOException {
+        return new ByteArrayInputStream(m_current);
+    }
+
+    public boolean restore() throws IOException {
+        if (m_backup == null) {
+            return false;
+        }
+        m_current = AdminTestUtil.copy(m_backup);
+        return true;
+    }
+
+    public void write(InputStream data) throws IOException {
+        m_current = AdminTestUtil.copy(data);
+    }
+}

Added: ace/sandbox/marrs/org.apache.ace.client.repository.impl/test/org/apache/ace/client/repository/impl/MockCachedRepository.java
URL: http://svn.apache.org/viewvc/ace/sandbox/marrs/org.apache.ace.client.repository.impl/test/org/apache/ace/client/repository/impl/MockCachedRepository.java?rev=1357570&view=auto
==============================================================================
--- ace/sandbox/marrs/org.apache.ace.client.repository.impl/test/org/apache/ace/client/repository/impl/MockCachedRepository.java (added)
+++ ace/sandbox/marrs/org.apache.ace.client.repository.impl/test/org/apache/ace/client/repository/impl/MockCachedRepository.java Thu Jul  5 12:09:30 2012
@@ -0,0 +1,85 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.ace.client.repository.impl;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import org.apache.ace.range.SortedRangeSet;
+import org.apache.ace.repository.ext.CachedRepository;
+
+
+public class MockCachedRepository implements CachedRepository {
+
+    public InputStream checkout(boolean fail) throws IOException {
+        // TODO Auto-generated method stub
+        return null;
+    }
+
+    public boolean commit() throws IOException {
+        // TODO Auto-generated method stub
+        return false;
+    }
+
+    public InputStream getLocal(boolean fail) throws IOException {
+        // TODO Auto-generated method stub
+        return null;
+    }
+
+    public boolean revert() throws IOException {
+        // TODO Auto-generated method stub
+        return false;
+    }
+
+    public void writeLocal(InputStream data) throws IOException {
+        // TODO Auto-generated method stub
+
+    }
+
+    public InputStream checkout(long version) throws IOException, IllegalArgumentException {
+        // TODO Auto-generated method stub
+        return null;
+    }
+
+    public boolean commit(InputStream data, long fromVersion) throws IOException, IllegalArgumentException {
+        // TODO Auto-generated method stub
+        return false;
+    }
+
+    public SortedRangeSet getRange() throws IOException {
+        // TODO Auto-generated method stub
+        return null;
+    }
+
+    public boolean isCurrent() throws IOException {
+        // TODO Auto-generated method stub
+        return false;
+    }
+
+    public long getHighestRemoteVersion() throws IOException {
+        // TODO Auto-generated method stub
+        return 0;
+    }
+
+    public long getMostRecentVersion() {
+        // TODO Auto-generated method stub
+        return 0;
+    }
+
+}

Added: ace/sandbox/marrs/org.apache.ace.client.repository.impl/test/org/apache/ace/client/repository/impl/MockRepository.java
URL: http://svn.apache.org/viewvc/ace/sandbox/marrs/org.apache.ace.client.repository.impl/test/org/apache/ace/client/repository/impl/MockRepository.java?rev=1357570&view=auto
==============================================================================
--- ace/sandbox/marrs/org.apache.ace.client.repository.impl/test/org/apache/ace/client/repository/impl/MockRepository.java (added)
+++ ace/sandbox/marrs/org.apache.ace.client.repository.impl/test/org/apache/ace/client/repository/impl/MockRepository.java Thu Jul  5 12:09:30 2012
@@ -0,0 +1,52 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.ace.client.repository.impl;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+import org.apache.ace.range.SortedRangeSet;
+import org.apache.ace.repository.Repository;
+
+
+public class MockRepository implements Repository {
+    private byte[] m_repo;
+    private int currentVersion = 0;
+
+    public InputStream checkout(long version) throws IOException, IllegalArgumentException {
+        if (version == currentVersion) {
+            return new ByteArrayInputStream(m_repo);
+        }
+        return null;
+    }
+
+    public boolean commit(InputStream data, long fromVersion) throws IOException, IllegalArgumentException {
+        if (fromVersion == currentVersion) {
+            currentVersion++;
+            m_repo = AdminTestUtil.copy(data);
+            return true;
+        }
+        return false;
+    }
+
+    public SortedRangeSet getRange() throws IOException {
+        return new SortedRangeSet(new long[] {currentVersion});
+    }
+}