incubator-ace-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From ma...@apache.org
Subject svn commit: r788992 [15/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...
Date Sat, 27 Jun 2009 15:53:26 GMT
Added: incubator/ace/trunk/server/src/net/luminis/liq/repository/impl/FilebasedBackupRepository.java
URL: http://svn.apache.org/viewvc/incubator/ace/trunk/server/src/net/luminis/liq/repository/impl/FilebasedBackupRepository.java?rev=788992&view=auto
==============================================================================
--- incubator/ace/trunk/server/src/net/luminis/liq/repository/impl/FilebasedBackupRepository.java (added)
+++ incubator/ace/trunk/server/src/net/luminis/liq/repository/impl/FilebasedBackupRepository.java Sat Jun 27 15:53:04 2009
@@ -0,0 +1,124 @@
+package net.luminis.liq.repository.impl;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import net.luminis.liq.repository.ext.BackupRepository;
+
+/**
+ * A file-based implementation of the Backup Repository, using two files to store the current
+ * and backup version.
+ */
+public class FilebasedBackupRepository implements BackupRepository {
+    private static final int COPY_BUFFER_SIZE = 4096;
+
+    private final File m_current;
+    private final File m_backup;
+
+    /**
+     * Creates a FilebasedBackupRepository. The file objects should point to a correct file,
+     * but the files will be created when necessary.
+     * @param current A file to store the current revision in.
+     * @param backup A file to store a backup version in.
+     */
+    public FilebasedBackupRepository(File current, File backup) {
+        m_current = current;
+        m_backup = backup;
+    }
+
+    public InputStream read() throws IOException {
+        if (!m_current.exists()) {
+            return new ByteArrayInputStream(new byte[0]);
+        }
+
+        try {
+            return new FileInputStream(m_current);
+        }
+        catch (FileNotFoundException e) {
+            throw new IOException("Unable to open file:" + e.getMessage());
+        }
+    }
+
+    public void write(InputStream data) throws IOException {
+        try {
+            if (!m_current.exists()) {
+                m_current.createNewFile();
+            }
+        }
+        catch (IOException e) {
+            throw new IOException("Unable to create file:" + e.getMessage());
+        }
+
+        try {
+            FileOutputStream out = new FileOutputStream(m_current);
+            copy(data, out);
+            out.close();
+        }
+        catch (FileNotFoundException e) {
+            throw new IOException("Unable to open file:" + e.getMessage());
+        }
+    }
+
+    public boolean backup() throws IOException {
+        if (!m_current.exists()) {
+            return false;
+        }
+        copy(m_current, m_backup);
+        return true;
+    }
+
+    public boolean restore() throws IOException {
+        if (!m_backup.exists()) {
+            return false;
+        }
+        copy(m_backup, m_current);
+        return true;
+    }
+
+    /**
+     * Helper function that writes the contents of one file to another.
+     * @param source The source file.
+     * @param destination The destination file.
+     * @throws IOException Thrown when file IO goes wrong.
+     */
+    private static void copy(File source, File destination) throws IOException {
+        if (destination.exists()) {
+            destination.delete();
+        }
+        destination.createNewFile();
+
+        FileOutputStream out = new FileOutputStream(destination);
+        FileInputStream in = new FileInputStream(source);
+
+        copy(in, out);
+        in.close();
+        out.close();
+    }
+
+    /**
+     * Copies the contents of an input stream to an output stream.
+     * @param in The input stream.
+     * @param out The output stream.
+     * @throws IOException Thrown when the output stream is closed unexpectedly.
+     */
+    private static void copy(InputStream in, OutputStream out) throws IOException {
+        byte[] buffer = new byte[COPY_BUFFER_SIZE];
+        int bytes = in.read(buffer);
+        while (bytes != -1) {
+            out.write(buffer, 0, bytes);
+            out.flush();
+            bytes = in.read(buffer);
+        }
+    }
+
+    @Override
+    public String toString() {
+        return "FilebasedBackupRepository[" + m_current + "," + m_backup + "]";
+    }
+}

Added: incubator/ace/trunk/server/src/net/luminis/liq/repository/impl/RemoteRepository.java
URL: http://svn.apache.org/viewvc/incubator/ace/trunk/server/src/net/luminis/liq/repository/impl/RemoteRepository.java?rev=788992&view=auto
==============================================================================
--- incubator/ace/trunk/server/src/net/luminis/liq/repository/impl/RemoteRepository.java (added)
+++ incubator/ace/trunk/server/src/net/luminis/liq/repository/impl/RemoteRepository.java Sat Jun 27 15:53:04 2009
@@ -0,0 +1,177 @@
+package net.luminis.liq.repository.impl;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.URL;
+
+import javax.servlet.http.HttpServletResponse;
+
+import net.luminis.liq.repository.Repository;
+import net.luminis.liq.repository.SortedRangeSet;
+
+
+/**
+ * This class works as a local interface for a remote repository by handling the network
+ * communication.
+ */
+public class RemoteRepository implements Repository {
+    private static final String COMMAND_QUERY = "/query";
+    private static final String COMMAND_CHECKOUT = "/checkout";
+    private static final String COMMAND_COMMIT = "/commit";
+    private static final String MIME_APPLICATION_OCTET_STREAM = "application/octet-stream";
+    private static final int COPY_BUFFER_SIZE = 4096;
+
+    private final URL m_url;
+    private final String m_customer;
+    private final String m_name;
+    private final String m_filter;
+
+
+    RemoteRepository(URL url, String customer, String name, String filter) {
+        m_url = url;
+        m_customer = customer;
+        m_name = name;
+        m_filter = filter;
+    }
+
+    /**
+     * Creates a remote repository that connects to a given location with a given customer-
+     * and repository name.
+     * @param url The location of the repository.
+     * @param customer The customer name to use.
+     * @param name The repository name to use.
+     */
+    public RemoteRepository(URL url, String customer, String name) {
+        this(url, customer, name, null);
+    }
+
+    /**
+     * Creates a remote repository that connects to a given location with a given filter.
+     * @param url The location of the repository.
+     * @param filter An LDAP filter string to select the repository.
+     */
+    public RemoteRepository(URL url, String filter) {
+        this(url, null, null, filter);
+    }
+
+
+    public InputStream checkout(long version) throws IOException, IllegalArgumentException {
+        if (version <= 0) {
+            throw new IllegalArgumentException("Version must be greater than 0.");
+        }
+
+        URL url = buildCommand(m_url, COMMAND_CHECKOUT, version);
+        HttpURLConnection connection = (HttpURLConnection) url.openConnection();
+        if (connection.getResponseCode() == HttpServletResponse.SC_NOT_FOUND) {
+            throw new IllegalArgumentException("Requested version not found in remote repository. (" + connection.getResponseMessage() + ")");
+        }
+        if (connection.getResponseCode() != HttpServletResponse.SC_OK) {
+            throw new IOException("Connection error: " + connection.getResponseMessage());
+        }
+
+        return connection.getInputStream();
+    }
+
+    public boolean commit(InputStream data, long fromVersion) throws IOException, IllegalArgumentException {
+        URL url = buildCommand(m_url, COMMAND_COMMIT, fromVersion);
+        HttpURLConnection connection = (HttpURLConnection) url.openConnection();
+        connection.setDoOutput(true);
+        connection.setRequestProperty("Content-Type", MIME_APPLICATION_OCTET_STREAM);
+
+        OutputStream out = connection.getOutputStream();
+        copy(data, out);
+        out.flush();
+        out.close();
+        return connection.getResponseCode() == HttpServletResponse.SC_OK;
+    }
+
+    public SortedRangeSet getRange() throws IOException {
+        URL url = buildCommand(m_url, COMMAND_QUERY, 0);
+        HttpURLConnection connection = (HttpURLConnection) url.openConnection();
+        if (connection.getResponseCode() == HttpServletResponse.SC_OK) {
+            BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
+            String line = reader.readLine();
+            if (line == null) {
+                throw new IOException("Repository not found: customer=" + m_customer + ", name=" + m_name);
+            }
+            String representation = line.substring(line.lastIndexOf(','));
+            reader.close();
+            return new SortedRangeSet(representation);
+        }
+        throw new IOException("Connection error: " + connection.getResponseMessage());
+    }
+
+    /**
+     * Helper method which copies the contents of an input stream to an output stream.
+     * @param in The input stream.
+     * @param out The output stream.
+     * @throws IOException Thrown when one of the streams is closed unexpectedly.
+     */
+    private static void copy(InputStream in, OutputStream out) throws IOException {
+        byte[] buffer = new byte[COPY_BUFFER_SIZE];
+        int bytes = in.read(buffer);
+        while (bytes != -1) {
+            out.write(buffer, 0, bytes);
+            bytes = in.read(buffer);
+        }
+    }
+
+    /**
+     * Builds a command string to use in the request to the server, based on the parameters
+     * this object was created with. The version is only mandatory for <code>CHECKOUT</code>
+     * and <code>COMMIT</code>.
+     * @param command A command string, use the <code>COMMAND_</code> constants in this file.
+     * @param version A version statement.
+     * @return The command string.
+     */
+    private URL buildCommand(URL url, String command, long version) {
+        StringBuffer result = new StringBuffer();
+
+        if (m_filter != null) {
+            result.append(m_filter);
+        }
+        else {
+
+            if (m_customer != null) {
+                if (result.length() != 0) {
+                    result.append("&");
+                }
+                result.append("customer=").append(m_customer);
+            }
+            if (m_name != null) {
+                if (result.length() != 0) {
+                    result.append("&");
+                }
+                result.append("name=").append(m_name);
+            }
+            if (command != COMMAND_QUERY) {
+                if (result.length() != 0) {
+                    result.append("&");
+                }
+                result.append("version=").append(version);
+            }
+        }
+
+        try {
+            if (result.length() > 0) {
+                return new URL(url.toString() + command + "?" + result.toString());
+            }
+            else {
+                return new URL(url.toString() + command);
+            }
+        }
+        catch (MalformedURLException e) {
+            throw new IllegalArgumentException("Could not create URL: " + e.getMessage());
+        }
+    }
+
+    @Override
+    public String toString() {
+        return "RemoteRepository[" + m_url + "," + m_customer + "," + m_name + "," + m_filter + "]";
+    }
+}

Added: incubator/ace/trunk/server/src/net/luminis/liq/repository/impl/RepositoryFactory.java
URL: http://svn.apache.org/viewvc/incubator/ace/trunk/server/src/net/luminis/liq/repository/impl/RepositoryFactory.java?rev=788992&view=auto
==============================================================================
--- incubator/ace/trunk/server/src/net/luminis/liq/repository/impl/RepositoryFactory.java (added)
+++ incubator/ace/trunk/server/src/net/luminis/liq/repository/impl/RepositoryFactory.java Sat Jun 27 15:53:04 2009
@@ -0,0 +1,165 @@
+package net.luminis.liq.repository.impl;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.IOException;
+import java.util.Dictionary;
+import java.util.HashMap;
+import java.util.Map;
+
+import net.luminis.liq.repository.Repository;
+import net.luminis.liq.repository.RepositoryReplication;
+import net.luminis.liq.repository.impl.constants.RepositoryConstants;
+
+import org.apache.felix.dependencymanager.DependencyManager;
+import org.apache.felix.dependencymanager.Service;
+import org.osgi.framework.BundleContext;
+import org.osgi.service.cm.ConfigurationException;
+import org.osgi.service.cm.ManagedServiceFactory;
+import org.osgi.service.log.LogService;
+import org.osgi.service.prefs.BackingStoreException;
+import org.osgi.service.prefs.Preferences;
+import org.osgi.service.prefs.PreferencesService;
+
+/**
+ * A <code>ManagedServiceFactory</code> responsible for creating a (<code>Replication</code>)<code>Repository</code>
+ * instance for each valid configuration that is received from the <code>ConfigurationAdmin</code>.
+ */
+public class RepositoryFactory implements ManagedServiceFactory {
+
+    private volatile LogService m_log;                     /* injected by dependency manager */
+    private volatile BundleContext m_context;              /* injected by dependency manager */
+    private volatile PreferencesService m_prefsService;    /* injected by dependency manager */
+
+    private File m_tempDir;
+    private File m_baseDir;
+    private Preferences m_prefs;
+    private final DependencyManager m_manager;
+    private final Map<String, Service> m_instances = new HashMap<String, Service>();
+
+    public RepositoryFactory(DependencyManager manager) {
+        m_manager = manager;
+    }
+
+    public synchronized void deleted(String pid) {
+        // pull service
+        Service service = m_instances.remove(pid);
+        if (service != null) {
+            m_manager.remove(service);
+        }
+
+        // remove persisted data
+        File dir = new File(m_baseDir, pid);
+        if (dir.isDirectory()) {
+            File[] files = dir.listFiles();
+            for (int i = 0; (files != null) && (i < files.length); i++) {
+                files[i].delete();
+            }
+            if (!dir.delete()) {
+                m_log.log(LogService.LOG_WARNING, "Unable to clean up files ( in " + dir.getAbsolutePath() + ") after removing repository");
+            }
+        }
+        try {
+            m_prefs.node(pid).removeNode();
+            m_prefs.sync();
+        }
+        catch (BackingStoreException e) {
+            // Not much we can do
+        }
+    }
+
+    public String getName() {
+        return "RepositoryFactory";
+    }
+
+    public synchronized void init() {
+        m_tempDir = m_context.getDataFile("tmp");
+        if ((m_tempDir != null) && !m_tempDir.isDirectory() && !m_tempDir.mkdirs()) {
+            throw new IllegalArgumentException("Unable to create temp directory (" + m_tempDir.getAbsolutePath() + ")");
+        }
+        m_baseDir = m_context.getDataFile("repos");
+        if ((m_baseDir != null) && !m_baseDir.isDirectory() && !m_baseDir.mkdirs()) {
+            throw new IllegalArgumentException("Unable to create base directory (" + m_baseDir.getAbsolutePath() + ")");
+        }
+    }
+
+    /**
+     * Creates a new instance if the supplied dictionary contains a valid configuration. A configuration is valid if
+     * <code>RepositoryConstants.REPOSITORY_NAME</code> and <code>RepositoryConstants.REPOSITORY_CUSTOMER</code> properties
+     * are present, not empty and the combination of the two is unique in respect to other previously created instances.
+     * Finally a property <code>RepositoryConstants.REPOSITORY_MASTER</code> should be present and be either <code>true</code>
+     * or <code>false</code>.
+     *
+     * @param pid A unique identifier for the instance, generated by <code>ConfigurationAdmin</code> normally.
+     * @param dict The configuration properties for the instance, see description above.
+     * @throws ConfigurationException If any of the above explanation fails <b>or</b>when there is an internal error creating the repository.
+     */
+    @SuppressWarnings("unchecked")
+    public synchronized void updated(String pid, Dictionary dict) throws ConfigurationException {
+        String name = (String) dict.get(RepositoryConstants.REPOSITORY_NAME);
+        if ((name == null) || "".equals(name)) {
+            throw new ConfigurationException(RepositoryConstants.REPOSITORY_NAME, "Repository name has to be specified.");
+        }
+
+        String customer = (String) dict.get(RepositoryConstants.REPOSITORY_CUSTOMER);
+        if ((customer == null) || "".equals(customer)) {
+            throw new ConfigurationException(RepositoryConstants.REPOSITORY_CUSTOMER, "Repository customer has to be specified.");
+        }
+
+        String master = (String) dict.get(RepositoryConstants.REPOSITORY_MASTER);
+        if (!("false".equalsIgnoreCase(master.trim()) || "true".equalsIgnoreCase(master.trim()))) {
+            throw new ConfigurationException(RepositoryConstants.REPOSITORY_MASTER, "Have to specify whether the repository is the master or a slave.");
+        }
+        boolean isMaster = Boolean.parseBoolean(master);
+
+        String initialContents = (String) dict.get(RepositoryConstants.REPOSITORY_INITIAL_CONTENT);
+
+        if (m_prefs == null) {
+            m_prefs = m_prefsService.getSystemPreferences();
+        }
+
+        String[] nodes;
+        try {
+            nodes = m_prefs.childrenNames();
+        }
+        catch (BackingStoreException e) {
+            throw new ConfigurationException("none", "Internal error while validating configuration.");
+        }
+        for (int i = 0; i < nodes.length; i++) {
+            Preferences node = m_prefs.node(nodes[i]);
+            if (name.equalsIgnoreCase(node.get(RepositoryConstants.REPOSITORY_NAME, "")) && name.equalsIgnoreCase(node.get(RepositoryConstants.REPOSITORY_CUSTOMER, ""))) {
+                throw new ConfigurationException("name and customer", "Name and customer combination already exists");
+            }
+        }
+
+        Preferences node = m_prefs.node(pid);
+        node.put(RepositoryConstants.REPOSITORY_NAME, name);
+        node.put(RepositoryConstants.REPOSITORY_CUSTOMER, customer);
+
+        Service service = m_instances.get(pid);
+        if (service == null) {
+            // new instance
+            File dir = new File(m_baseDir, pid);
+            RepositoryImpl store = new RepositoryImpl(dir, m_tempDir, isMaster);
+            if ((initialContents != null) && isMaster) {
+                try {
+                    store.commit(new ByteArrayInputStream(initialContents.getBytes()), 0);
+                }
+                catch (IOException e) {
+                    m_log.log(LogService.LOG_ERROR, "Unable to set initial contents of the repository.", e);
+                }
+            }
+            service = m_manager.createService()
+                .setInterface(new String[] {RepositoryReplication.class.getName(), Repository.class.getName()}, dict)
+                .setImplementation(store)
+                .add(m_manager.createServiceDependency().setService(LogService.class).setRequired(false));
+            m_manager.add(service);
+            m_instances.put(pid, service);
+        } else {
+            // update existing instance
+            RepositoryImpl store = (RepositoryImpl) service.getService();
+            store.updated(isMaster);
+        }
+    }
+
+}

Added: incubator/ace/trunk/server/src/net/luminis/liq/repository/impl/RepositoryImpl.java
URL: http://svn.apache.org/viewvc/incubator/ace/trunk/server/src/net/luminis/liq/repository/impl/RepositoryImpl.java?rev=788992&view=auto
==============================================================================
--- incubator/ace/trunk/server/src/net/luminis/liq/repository/impl/RepositoryImpl.java (added)
+++ incubator/ace/trunk/server/src/net/luminis/liq/repository/impl/RepositoryImpl.java Sat Jun 27 15:53:04 2009
@@ -0,0 +1,182 @@
+package net.luminis.liq.repository.impl;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Arrays;
+
+import net.luminis.liq.repository.Repository;
+import net.luminis.liq.repository.RepositoryReplication;
+import net.luminis.liq.repository.SortedRangeSet;
+
+import org.osgi.service.cm.ConfigurationException;
+import org.osgi.service.log.LogService;
+
+/**
+ * Implementation of an object repository. The object repository holds (big) chunks of data identified by
+ * a version. To interact with the repository two interfaces are implemented:
+ * <ul>
+ *   <li><code>Repository</code> - a read-write interface to the repository, you can commit and checkout versions</li>
+ *   <li><code>RepositoryReplication</code> - interface used only for replication of the repository, you can get and put versions</li>
+ * </ul>
+ * A repository can be either a master or a slave repository. Committing a new version is only possible on a master repository.
+ */
+public class RepositoryImpl implements RepositoryReplication, Repository {
+
+    private volatile LogService m_log; /* will be injected by dependency manager */
+
+    private final File m_tempDir;
+    private File m_dir;
+    private boolean m_isMaster;
+
+    /**
+     * Creates a new repository.
+     *
+     * @param dir Directory to be used for storage of the repository data, will be created if needed.
+     * @param temp Directory to be used as temp directory, will be created if needed.
+     * @param isMaster True if this repository is a master repository, false otherwise.
+     * @throws IllegalArgumentException If <code>dir</code> and/or <code>temp</code> could not be created or is not a directory.
+     */
+    public RepositoryImpl(File dir, File temp, boolean isMaster) {
+        m_isMaster = isMaster;
+        if (!dir.isDirectory() && !dir.mkdirs()) {
+            throw new IllegalArgumentException("Repository location is not a valid directory (" + dir.getAbsolutePath() + ")");
+        }
+        if (!temp.isDirectory() && !temp.mkdirs()) {
+            throw new IllegalArgumentException("Temp location is not a valid directory (" + temp.getAbsolutePath() + ")");
+        }
+        m_tempDir = temp;
+        m_dir = dir;
+    }
+
+    public InputStream get(long version) throws IOException, IllegalArgumentException {
+        return checkout(version);
+    }
+
+    public boolean put(InputStream data, long version) throws IOException, IllegalArgumentException {
+        if (version <= 0) {
+            throw new IllegalArgumentException("Version must be greater than 0.");
+        }
+        File file = new File(m_dir, Long.toString(version));
+        if (file.exists()) {
+            return false;
+        }
+
+        // store stream in temp file
+        File tempFile = File.createTempFile("repository", null, m_tempDir);
+        OutputStream fileStream = null;
+        try {
+            fileStream = new FileOutputStream(tempFile);
+            byte[] buffer = new byte[1024];
+            int bytes = data.read(buffer);
+            while (bytes != -1) {
+                fileStream.write(buffer, 0, bytes);
+                bytes = data.read(buffer);
+            }
+        }
+        catch (IOException e) {
+            String deleteMsg = "";
+            if (!tempFile.delete()) {
+                deleteMsg = " and was unable to remove temp file " + tempFile.getAbsolutePath();
+            }
+            m_log.log(LogService.LOG_WARNING, "Error occurred while storing new version in repository" + deleteMsg, e);
+            throw e;
+        }
+        finally {
+            if (fileStream != null) {
+                try {
+                    fileStream.close();
+                }
+                catch (IOException ioe) {
+                    // Not much we can do
+                }
+            }
+        }
+
+        // move temp file to final location
+        if (!tempFile.renameTo(file)) {
+            String deleteMsg = "";
+            if (!tempFile.delete()) {
+                deleteMsg = " and was unable to remove temp file " + tempFile.getAbsolutePath();
+            }
+            throw new IOException("Unable to move temp file (" + tempFile.getAbsolutePath() + ") to final location (" + file.getAbsolutePath() + ")" + deleteMsg);
+        }
+
+        return true;
+    }
+
+    public InputStream checkout(long version) throws IOException, IllegalArgumentException {
+        if (version <= 0) {
+            throw new IllegalArgumentException("Version must be greater than 0.");
+        }
+        File file = new File(m_dir, String.valueOf(version));
+        return (file.isFile()) ? new FileInputStream(file) : null;
+    }
+
+    public boolean commit(InputStream data, long fromVersion) throws IOException, IllegalArgumentException {
+        if (!m_isMaster) {
+            throw new IllegalStateException("Commit is only permitted on master repositories");
+        }
+        if (fromVersion < 0) {
+            throw new IllegalArgumentException("Version must be greater than or equal to 0.");
+        }
+
+        long[] versions = getVersions();
+
+        if (versions.length == 0) {
+            if (fromVersion == 0) {
+                put(data, 1);
+                return true;
+            } else {
+                return false;
+            }
+        }
+
+        long lastVersion = versions[versions.length - 1];
+        if (lastVersion == fromVersion) {
+            put(data, fromVersion + 1);
+            return true;
+        }
+        else {
+            return false;
+        }
+    }
+
+    public SortedRangeSet getRange() throws IOException {
+        return new SortedRangeSet(getVersions());
+    }
+
+    /* returns list of all versions present in ascending order */
+    private long[] getVersions() throws IOException {
+        File[] versions = m_dir.listFiles();
+        if (versions == null) {
+            throw new IOException("Unable to list version in the store (failed to get the filelist for directory '" + m_dir.getAbsolutePath() + "'");
+        }
+        long[] results = new long[versions.length];
+        for (int i = 0; i < versions.length; i++) {
+            try {
+                results[i] = Long.parseLong(versions[i].getName());
+            }
+            catch (NumberFormatException nfe) {
+                m_log.log(LogService.LOG_WARNING, "Unable to determine version number for '" + results[i] + "', skipping it.");
+            }
+        }
+        Arrays.sort(results);
+        return results;
+    }
+
+    /**
+     * Updates the repository configuration.
+     *
+     * @param isMaster True if the repository is a master repository, false otherwise.
+     *
+     * @throws ConfigurationException If it was impossible to use the new configuration.
+     */
+    public synchronized void updated(boolean isMaster) throws ConfigurationException {
+        m_isMaster = isMaster;
+    }
+
+}

Added: incubator/ace/trunk/server/src/net/luminis/liq/repository/impl/constants/RepositoryConstants.java
URL: http://svn.apache.org/viewvc/incubator/ace/trunk/server/src/net/luminis/liq/repository/impl/constants/RepositoryConstants.java?rev=788992&view=auto
==============================================================================
--- incubator/ace/trunk/server/src/net/luminis/liq/repository/impl/constants/RepositoryConstants.java (added)
+++ incubator/ace/trunk/server/src/net/luminis/liq/repository/impl/constants/RepositoryConstants.java Sat Jun 27 15:53:04 2009
@@ -0,0 +1,8 @@
+package net.luminis.liq.repository.impl.constants;
+
+public interface RepositoryConstants {
+    public static final String REPOSITORY_NAME = "name";
+    public static final String REPOSITORY_CUSTOMER = "customer";
+    public static final String REPOSITORY_MASTER = "master";
+    public static final String REPOSITORY_INITIAL_CONTENT = "initial";
+}

Added: incubator/ace/trunk/server/src/net/luminis/liq/repository/servlet/Activator.java
URL: http://svn.apache.org/viewvc/incubator/ace/trunk/server/src/net/luminis/liq/repository/servlet/Activator.java?rev=788992&view=auto
==============================================================================
--- incubator/ace/trunk/server/src/net/luminis/liq/repository/servlet/Activator.java (added)
+++ incubator/ace/trunk/server/src/net/luminis/liq/repository/servlet/Activator.java Sat Jun 27 15:53:04 2009
@@ -0,0 +1,42 @@
+package net.luminis.liq.repository.servlet;
+
+
+import javax.servlet.http.HttpServlet;
+
+import org.apache.felix.dependencymanager.DependencyActivatorBase;
+import org.apache.felix.dependencymanager.DependencyManager;
+import org.osgi.framework.BundleContext;
+import org.osgi.service.log.LogService;
+
+public class Activator extends DependencyActivatorBase {
+    public static final String REPOSITORY_PID = "net.luminis.liq.repository.servlet.RepositoryServlet";
+    public static final String REPOSITORY_REPLICATION_PID = "net.luminis.liq.repository.servlet.RepositoryReplicationServlet";
+
+    @Override
+    public void init(BundleContext context, DependencyManager manager) throws Exception {
+        manager.add(createService()
+            .setInterface(HttpServlet.class.getName(), null)
+            .setImplementation(RepositoryServlet.class)
+            .add(createConfigurationDependency()
+                .setPropagate(true)
+                .setPid(REPOSITORY_PID))
+            .add(createServiceDependency()
+                .setService(LogService.class)
+                .setRequired(false)));
+
+        manager.add(createService()
+            .setInterface(HttpServlet.class.getName(), null)
+            .setImplementation(RepositoryReplicationServlet.class)
+            .add(createConfigurationDependency()
+                .setPropagate(true)
+                .setPid(REPOSITORY_REPLICATION_PID))
+            .add(createServiceDependency()
+                .setService(LogService.class)
+                .setRequired(false)));
+    }
+
+    @Override
+    public void destroy(BundleContext context, DependencyManager manager) throws Exception {
+        // do nothing
+    }
+}

Added: incubator/ace/trunk/server/src/net/luminis/liq/repository/servlet/RepositoryReplicationServlet.java
URL: http://svn.apache.org/viewvc/incubator/ace/trunk/server/src/net/luminis/liq/repository/servlet/RepositoryReplicationServlet.java?rev=788992&view=auto
==============================================================================
--- incubator/ace/trunk/server/src/net/luminis/liq/repository/servlet/RepositoryReplicationServlet.java (added)
+++ incubator/ace/trunk/server/src/net/luminis/liq/repository/servlet/RepositoryReplicationServlet.java Sat Jun 27 15:53:04 2009
@@ -0,0 +1,66 @@
+package net.luminis.liq.repository.servlet;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Dictionary;
+
+import net.luminis.liq.repository.RepositoryReplication;
+import net.luminis.liq.repository.SortedRangeSet;
+
+import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.framework.ServiceReference;
+import org.osgi.service.cm.ConfigurationException;
+
+public class RepositoryReplicationServlet extends RepositoryServletBase {
+    private static final long serialVersionUID = 1L;
+
+    @Override
+    protected ServiceReference[] getRepositories(String filter) throws InvalidSyntaxException {
+        return m_context.getServiceReferences(RepositoryReplication.class.getName(), filter);
+    }
+
+    @Override
+    protected SortedRangeSet getRange(ServiceReference ref) throws IOException {
+        RepositoryReplication repository = (RepositoryReplication) m_context.getService(ref);
+        SortedRangeSet range = repository.getRange();
+        m_context.ungetService(ref);
+        return range;
+    }
+
+    @Override
+    protected boolean doCommit(ServiceReference ref, long version, InputStream data) throws IllegalArgumentException, IOException {
+        RepositoryReplication r = (RepositoryReplication) m_context.getService(ref);
+        boolean result = r.put(data, version);
+        m_context.ungetService(ref);
+        return result;
+    }
+
+    @Override
+    protected InputStream doCheckout(ServiceReference ref, long version) throws IllegalArgumentException, IOException {
+        RepositoryReplication r = (RepositoryReplication) m_context.getService(ref);
+        InputStream result = r.get(version);
+        m_context.ungetService(ref);
+        return result;
+    }
+
+    @Override
+    public String getServletInfo() {
+        return "LiQ Repository Replication Servlet";
+    }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public void updated(Dictionary settings) throws ConfigurationException {
+        // nothing special we want to do here
+    }
+
+    @Override
+    protected String getCheckoutCommand() {
+        return "/get";
+    }
+
+    @Override
+    protected String getCommitCommand() {
+        return "/put";
+    }
+}

Added: incubator/ace/trunk/server/src/net/luminis/liq/repository/servlet/RepositoryServlet.java
URL: http://svn.apache.org/viewvc/incubator/ace/trunk/server/src/net/luminis/liq/repository/servlet/RepositoryServlet.java?rev=788992&view=auto
==============================================================================
--- incubator/ace/trunk/server/src/net/luminis/liq/repository/servlet/RepositoryServlet.java (added)
+++ incubator/ace/trunk/server/src/net/luminis/liq/repository/servlet/RepositoryServlet.java Sat Jun 27 15:53:04 2009
@@ -0,0 +1,66 @@
+package net.luminis.liq.repository.servlet;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Dictionary;
+
+import net.luminis.liq.repository.Repository;
+import net.luminis.liq.repository.SortedRangeSet;
+
+import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.framework.ServiceReference;
+import org.osgi.service.cm.ConfigurationException;
+
+public class RepositoryServlet extends RepositoryServletBase {
+    private static final long serialVersionUID = 1L;
+
+    @Override
+    protected ServiceReference[] getRepositories(String filter) throws InvalidSyntaxException {
+        return m_context.getServiceReferences(Repository.class.getName(), filter);
+    }
+
+    @Override
+    protected SortedRangeSet getRange(ServiceReference ref) throws IOException {
+        Repository repository = (Repository) m_context.getService(ref);
+        SortedRangeSet range = repository.getRange();
+        m_context.ungetService(ref);
+        return range;
+    }
+
+    @Override
+    protected boolean doCommit(ServiceReference ref, long version, InputStream data) throws IllegalArgumentException, IOException {
+        Repository r = (Repository) m_context.getService(ref);
+        boolean result = r.commit(data, version);
+        m_context.ungetService(ref);
+        return result;
+    }
+
+    @Override
+    protected InputStream doCheckout(ServiceReference ref, long version) throws IllegalArgumentException, IOException {
+        Repository r = (Repository) m_context.getService(ref);
+        InputStream result = r.checkout(version);
+        m_context.ungetService(ref);
+        return result;
+    }
+
+    @Override
+    public String getServletInfo() {
+        return "LiQ Repository Servlet";
+    }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public void updated(Dictionary settings) throws ConfigurationException {
+        // nothing special we want to do here
+    }
+
+    @Override
+    protected String getCheckoutCommand() {
+        return "/checkout";
+    }
+
+    @Override
+    protected String getCommitCommand() {
+        return "/commit";
+    }
+}

Added: incubator/ace/trunk/server/src/net/luminis/liq/repository/servlet/RepositoryServletBase.java
URL: http://svn.apache.org/viewvc/incubator/ace/trunk/server/src/net/luminis/liq/repository/servlet/RepositoryServletBase.java?rev=788992&view=auto
==============================================================================
--- incubator/ace/trunk/server/src/net/luminis/liq/repository/servlet/RepositoryServletBase.java (added)
+++ incubator/ace/trunk/server/src/net/luminis/liq/repository/servlet/RepositoryServletBase.java Sat Jun 27 15:53:04 2009
@@ -0,0 +1,256 @@
+package net.luminis.liq.repository.servlet;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Dictionary;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import net.luminis.liq.repository.SortedRangeSet;
+
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.framework.ServiceReference;
+import org.osgi.service.cm.ConfigurationException;
+import org.osgi.service.cm.ManagedService;
+
+/**
+ * Base class for the repository servlets. Both the repository and the repository replication
+ * servlets work in a similar way, so the specifics were factored out of this base class and
+ * put in two subclasses.
+ */
+public abstract class RepositoryServletBase extends HttpServlet implements ManagedService {
+    private static final int COPY_BUFFER_SIZE = 1024;
+    private static final String QUERY = "/query";
+    protected static final String TEXT_MIMETYPE = "text/plain";
+    protected static final String BINARY_MIMETYPE = "application/octet-stream";
+
+    protected volatile BundleContext m_context;
+
+    @Override
+    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+        String path = request.getPathInfo();
+        String customer = request.getParameter("customer");
+        String name = request.getParameter("name");
+        String filter = request.getParameter("filter");
+        String version = request.getParameter("version");
+
+        if (QUERY.equals(path)) {
+            // both repositories have a query method
+            if (filter != null) {
+                if ((name == null) && (customer == null)) {
+                    handleQuery(filter, response);
+                }
+                else {
+                    response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Specify either a filter or customer and/or name, but not both.");
+                }
+            }
+            else {
+                if ((name != null) && (customer != null)) {
+                    handleQuery("(&(customer=" + customer + ")(name=" + name + "))", response);
+                }
+                else if (name != null) {
+                    handleQuery("(name=" + name + ")", response);
+                }
+                else if (customer != null) {
+                    handleQuery("(customer=" + customer + ")", response);
+                }
+                else {
+                    handleQuery(null, response);
+                }
+            }
+        }
+        else if (getCheckoutCommand().equals(path)) {
+            // and both have a checkout, only it's named differently
+            if ((name != null) && (customer != null) && (version != null)) {
+                handleCheckout(customer, name, Long.parseLong(version), response);
+            }
+        }
+        else {
+            response.sendError(HttpServletResponse.SC_NOT_FOUND);
+        }
+    }
+
+    /**
+     * Returns the name of the "checkout" command.
+     */
+    protected abstract String getCheckoutCommand();
+
+    @Override
+    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+        String path = request.getPathInfo();
+        String customer = request.getParameter("customer");
+        String name = request.getParameter("name");
+        String version = request.getParameter("version");
+
+        if (getCommitCommand().equals(path)) {
+            // and finally, both have a commit, only it's named differently
+            if ((name != null) && (customer != null) && (version != null)) {
+                handleCommit(customer, name, Long.parseLong(version), request.getInputStream(), response);
+            }
+            else {
+                response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Name, customer and version should all be specified.");
+            }
+        }
+        else {
+            response.sendError(HttpServletResponse.SC_NOT_FOUND);
+        }
+    }
+
+    /**
+     * Returns the name of the "commit" command.
+     */
+    protected abstract String getCommitCommand();
+
+    /**
+     * Handles a query command and sends back the response.
+     */
+    private void handleQuery(String filter, HttpServletResponse response) throws IOException {
+        try {
+            ServiceReference[] refs = getRepositories(filter);
+            StringBuffer result = new StringBuffer();
+            if (refs != null) {
+                for (ServiceReference ref : refs) {
+                    result.append((String) ref.getProperty("customer"));
+                    result.append(',');
+                    result.append((String) ref.getProperty("name"));
+                    result.append(',');
+                    result.append(getRange(ref).toRepresentation());
+                    result.append('\n');
+                }
+            }
+            response.setContentType(TEXT_MIMETYPE);
+            response.getWriter().print(result.toString());
+        }
+        catch (IOException e) {
+            response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Could not retrieve version range for repository: " + e.getMessage());
+        }
+        catch (InvalidSyntaxException e) {
+            response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Invalid filter syntax: " + e.getMessage());
+        }
+    }
+
+    /**
+     * Implement this by asking the right repository for a range of available versions.
+     *
+     * @param ref reference to the repository service you need to dereference
+     * @return a sorted range set
+     * @throws IOException if the range cannot be obtained
+     */
+    protected abstract SortedRangeSet getRange(ServiceReference ref) throws IOException;
+
+    /**
+     * Returns a list of repositories that match the specified filter condition.
+     *
+     * @param filter the filter condition
+     * @return an array of service references
+     * @throws InvalidSyntaxException if the filter condition is invalid
+     */
+    protected abstract ServiceReference[] getRepositories(String filter) throws InvalidSyntaxException;
+
+    /**
+     * Handles a commit command and sends back the response.
+     */
+    private void handleCommit(String customer, String name, long version, InputStream data, HttpServletResponse response) throws IOException {
+        try {
+            ServiceReference[] refs = getRepositories("(&(customer=" + customer + ")(name=" + name + "))");
+            if ((refs != null) && (refs.length == 1)) {
+                ServiceReference ref = refs[0];
+                try {
+                    if (!doCommit(ref, version, data)) {
+                        response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Could not commit");
+                    } else {
+                        response.sendError(HttpServletResponse.SC_OK);
+                    }
+                }
+                catch (IllegalArgumentException e) {
+                    response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Invalid version");
+                }
+                catch (IllegalStateException e) {
+                    response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Cannot commit, not the master repository");
+                }
+            }
+        }
+        catch (IOException e) {
+            response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "I/O exception: " + e.getMessage());
+        }
+        catch (InvalidSyntaxException e) {
+            response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Invalid filter syntax: " + e.getMessage());
+        }
+    }
+
+    /**
+     * Commit or put the data into the repository.
+     *
+     * @param ref reference to the repository service
+     * @param version the version
+     * @param data the data
+     * @return <code>true</code> if successful
+     * @throws IllegalArgumentException
+     * @throws IOException
+     */
+    protected abstract boolean doCommit(ServiceReference ref, long version, InputStream data) throws IllegalArgumentException, IOException;
+
+    /**
+     * Handles a checkout command and returns the response.
+     */
+    private void handleCheckout(String customer, String name, long version, HttpServletResponse response) throws IOException {
+        try {
+            ServiceReference[] refs = getRepositories("(&(customer=" + customer + ")(name=" + name + "))");
+            if ((refs != null) && (refs.length == 1)) {
+                ServiceReference ref = refs[0];
+                response.setContentType(BINARY_MIMETYPE);
+                InputStream data = doCheckout(ref, version);
+                if (data == null) {
+                    response.sendError(HttpServletResponse.SC_NOT_FOUND, "Requested version does not exist: " + version);
+                } else {
+                    copy(data, response.getOutputStream());
+                }
+            }
+            else {
+                response.sendError(HttpServletResponse.SC_NOT_FOUND, ((refs == null) ? "Could not find repository " : "Multiple repositories found ") + " for customer " + customer + ", name " + name);
+            }
+        }
+        catch (IOException e) {
+            response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "I/O exception: " + e.getMessage());
+        }
+        catch (InvalidSyntaxException e) {
+            response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Invalid filter syntax: " + e.getMessage());
+        }
+    }
+
+    /**
+     * Checkout or get data from the repository.
+     *
+     * @param ref reference to the repository service
+     * @param version the version
+     * @return the data
+     * @throws IllegalArgumentException
+     * @throws IOException
+     */
+    protected abstract InputStream doCheckout(ServiceReference ref, long version) throws IllegalArgumentException, IOException;
+
+    /**
+     * Copies data from an input stream to an output stream.
+     * @param in the input
+     * @param out the output
+     * @throws IOException if copying fails
+     */
+    private void copy(InputStream in, OutputStream out) throws IOException {
+        byte[] buffer = new byte[COPY_BUFFER_SIZE];
+        int bytes = in.read(buffer);
+        while (bytes != -1) {
+            out.write(buffer, 0, bytes);
+            bytes = in.read(buffer);
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    public void updated(Dictionary settings) throws ConfigurationException {
+        // nothing special we want to do here, dependency manager will do the propagation
+    }
+}

Added: incubator/ace/trunk/server/src/net/luminis/liq/repository/task/Activator.java
URL: http://svn.apache.org/viewvc/incubator/ace/trunk/server/src/net/luminis/liq/repository/task/Activator.java?rev=788992&view=auto
==============================================================================
--- incubator/ace/trunk/server/src/net/luminis/liq/repository/task/Activator.java (added)
+++ incubator/ace/trunk/server/src/net/luminis/liq/repository/task/Activator.java Sat Jun 27 15:53:04 2009
@@ -0,0 +1,30 @@
+package net.luminis.liq.repository.task;
+
+import java.util.Properties;
+
+import net.luminis.liq.discovery.Discovery;
+
+import org.apache.felix.dependencymanager.DependencyActivatorBase;
+import org.apache.felix.dependencymanager.DependencyManager;
+import org.osgi.framework.BundleContext;
+import org.osgi.service.log.LogService;
+
+public class Activator extends DependencyActivatorBase {
+    @Override
+    public void init(BundleContext context, DependencyManager manager) throws Exception {
+        Properties props = new Properties();
+        // TODO we need to fix these property constants
+        props.put("taskName", RepositoryReplicationTask.class.getName());
+        props.put("description", "Synchronizes repositories.");
+        manager.add(createService()
+            .setInterface(Runnable.class.getName(), props)
+            .setImplementation(RepositoryReplicationTask.class)
+            .add(createServiceDependency().setService(Discovery.class).setRequired(true))
+            .add(createServiceDependency().setService(LogService.class).setRequired(false))
+            );
+    }
+
+    @Override
+    public void destroy(BundleContext context, DependencyManager manager) throws Exception {
+    }
+}

Added: incubator/ace/trunk/server/src/net/luminis/liq/repository/task/RepositoryReplicationTask.java
URL: http://svn.apache.org/viewvc/incubator/ace/trunk/server/src/net/luminis/liq/repository/task/RepositoryReplicationTask.java?rev=788992&view=auto
==============================================================================
--- incubator/ace/trunk/server/src/net/luminis/liq/repository/task/RepositoryReplicationTask.java (added)
+++ incubator/ace/trunk/server/src/net/luminis/liq/repository/task/RepositoryReplicationTask.java Sat Jun 27 15:53:04 2009
@@ -0,0 +1,73 @@
+package net.luminis.liq.repository.task;
+
+import java.io.BufferedReader;
+import java.io.InputStreamReader;
+import java.net.HttpURLConnection;
+import java.net.URL;
+
+import javax.servlet.http.HttpServletResponse;
+
+import net.luminis.liq.discovery.Discovery;
+import net.luminis.liq.repository.RangeIterator;
+import net.luminis.liq.repository.RepositoryReplication;
+import net.luminis.liq.repository.SortedRangeSet;
+
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceReference;
+import org.osgi.service.log.LogService;
+
+/**
+ * Repository replication task. Uses discovery to find the server it talks to.
+ * Subsequently it checks which local repositories are configured and tries to
+ * synchronize them with remote copies. Only pulls stuff in, it does not push
+ * stuff out.
+ */
+public class RepositoryReplicationTask implements Runnable {
+    private volatile Discovery m_discovery;
+    private volatile LogService m_log;
+    private volatile BundleContext m_context;
+
+    public void run() {
+        try {
+            URL host = m_discovery.discover();
+            ServiceReference[] refs = m_context.getServiceReferences(RepositoryReplication.class.getName(), null);
+            for (ServiceReference ref : refs) {
+                RepositoryReplication repository = (RepositoryReplication) m_context.getService(ref);
+                SortedRangeSet localRange = repository.getRange();
+                Object customer = ref.getProperty("customer");
+                Object name = ref.getProperty("name");
+                String filter = "customer=" + customer + "&name=" + name;
+                URL query = new URL(host, "/replication/query?" + filter);
+                HttpURLConnection connection = (HttpURLConnection) query.openConnection();
+                if (connection.getResponseCode() == HttpServletResponse.SC_OK) {
+                    BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
+                    try {
+                        String line = reader.readLine();
+                        int i = line.lastIndexOf(',');
+                        if (i > 0) {
+                            SortedRangeSet remoteRange = new SortedRangeSet(line.substring(i + 1));
+                            SortedRangeSet delta = localRange.diffDest(remoteRange);
+                            RangeIterator iterator = delta.iterator();
+                            while (iterator.hasNext()) {
+                                long version = iterator.next();
+                                URL get = new URL(host, "/replication/get?" + filter + "&version=" + version);
+                                HttpURLConnection connection2 = (HttpURLConnection) get.openConnection();
+                                repository.put(connection2.getInputStream(), version);
+                            }
+                        }
+                    }
+                    catch (Exception e) {
+                        m_log.log(LogService.LOG_WARNING, "Error parsing remote range", e);
+                    }
+                }
+                else {
+                    m_log.log(LogService.LOG_WARNING, "Could not sync repository for customer " + customer + " name " + name + " because: " + connection.getResponseMessage() + " (" + connection.getResponseCode() + ")");
+                }
+                m_context.ungetService(ref);
+            }
+        }
+        catch (Exception e) {
+            m_log.log(LogService.LOG_WARNING, "Error while replicating", e);
+        }
+    }
+}

Added: incubator/ace/trunk/server/src/net/luminis/liq/resourceprocessor/useradmin/UserAdminConfigurator.java
URL: http://svn.apache.org/viewvc/incubator/ace/trunk/server/src/net/luminis/liq/resourceprocessor/useradmin/UserAdminConfigurator.java?rev=788992&view=auto
==============================================================================
--- incubator/ace/trunk/server/src/net/luminis/liq/resourceprocessor/useradmin/UserAdminConfigurator.java (added)
+++ incubator/ace/trunk/server/src/net/luminis/liq/resourceprocessor/useradmin/UserAdminConfigurator.java Sat Jun 27 15:53:04 2009
@@ -0,0 +1,74 @@
+package net.luminis.liq.resourceprocessor.useradmin;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import org.w3c.dom.Document;
+
+/**
+ * The UserAdminConfigurator can be used to install, remove or explicitly set the users that
+ * should be present in the system's UserAdmin.<br>
+ * <br>
+ * The document should have the following shape,
+ * <pre>
+ * &lt;roles&gt;
+ *     &lt;group name="group1"/&gt;
+ *     &lt;group name="group2"&gt;
+ *         &lt;memberof&gt;group1&lt;/memberof&gt;
+ *     &lt;/group&gt;
+ *     &lt;user name="user1"&gt;
+ *         &lt;properties&gt;
+ *             &lt;realname type="String"&gt;Mr. U. One&lt;/realname&gt;
+ *             &lt;address&gt;1 Infinite Loop&lt;/realname&gt;
+ *         &lt;/properties&gt;
+ *         &lt;credentials&gt;
+ *             &lt;password type="byte[]"&gt;secret&lt;/password&gt;
+ *         &lt;/credentials&gt;
+ *         &lt;memberof&gt;group1&lt;/memberof&gt;
+ *     &lt;/user&gt;
+ * &lt;/roles&gt;
+ * </pre>
+ * Note that when 'type' is missing in the values for properties or credentials, "String" will be assumed.
+ * <br>
+ * When no UserAdmin is available at time of installation, the UserAdminStore will keep the
+ * data around until one is, and update it with all data it has received up to then.
+ * Note that UserAdminStore is intended to work with one UserAdmin at a time.
+ */
+public interface UserAdminConfigurator {
+    /**
+     * Installs all roles found in a document.
+     * @param doc The document.
+     */
+    public void install(Document doc);
+    /**
+     * Installs all roles found in a document.
+     * @param input A stream containing the document.
+     * @throws IOException When there is a problem retrieving the document from the stream.
+     */
+    public void install(InputStream input) throws IOException;
+    /**
+     * Removes all roles found in a document.
+     * @param doc The document.
+     */
+    public void uninstall(Document doc);
+    /**
+     * Uninstalls all roles found in a document.
+     * @param input A stream containing the document.
+     * @throws IOException When there is a problem retrieving the document from the stream.
+     */
+    public void uninstall(InputStream input) throws IOException;
+
+    /**
+     * Sets the users found in a document as the only users to be present
+     * in the UserAdmin.
+     * @param doc The document.
+     */
+    public void setUsers(Document doc);
+    /**
+     * Sets the users found in a document as the only users to be present
+     * in the UserAdmin.
+     * @param input A stream containing the document.
+     * @throws IOException When there is a problem retrieving the document from the stream.
+     */
+    public void setUsers(InputStream input) throws IOException;
+}

Added: incubator/ace/trunk/server/src/net/luminis/liq/resourceprocessor/useradmin/impl/Activator.java
URL: http://svn.apache.org/viewvc/incubator/ace/trunk/server/src/net/luminis/liq/resourceprocessor/useradmin/impl/Activator.java?rev=788992&view=auto
==============================================================================
--- incubator/ace/trunk/server/src/net/luminis/liq/resourceprocessor/useradmin/impl/Activator.java (added)
+++ incubator/ace/trunk/server/src/net/luminis/liq/resourceprocessor/useradmin/impl/Activator.java Sat Jun 27 15:53:04 2009
@@ -0,0 +1,56 @@
+package net.luminis.liq.resourceprocessor.useradmin.impl;
+
+import java.util.Properties;
+
+import net.luminis.liq.resourceprocessor.useradmin.UserAdminConfigurator;
+
+import org.apache.felix.dependencymanager.DependencyActivatorBase;
+import org.apache.felix.dependencymanager.DependencyManager;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.Constants;
+import org.osgi.service.deploymentadmin.spi.ResourceProcessor;
+import org.osgi.service.log.LogService;
+import org.osgi.service.useradmin.UserAdmin;
+
+/**
+ * Activator for the UserAdmin resource processor. The services of this bundle
+ * will be published as a UserAdminConfigurator, and a ResourceProcessor for use
+ * by the Deployment Admin.
+ */
+public class Activator extends DependencyActivatorBase {
+    private static final String PID = "net.luminis.liq.resourceprocessor.useradmin";
+
+    @Override
+    public void init(BundleContext context, DependencyManager manager) {
+        UserAdminStore userAdminStore = new UserAdminStore(context);
+        Processor processor = new Processor(userAdminStore);
+
+        Properties props = new Properties();
+        props.put(Constants.SERVICE_PID, PID);
+        manager.add(createService().setInterface(ResourceProcessor.class.getName(), props)
+                .setImplementation(processor)
+                .add(createServiceDependency()
+                    .setService(UserAdminConfigurator.class)
+                    .setRequired(true)) // This UserAdminConfigurator is the same as below,
+                                        // and we don't want to add UserAdmins twice.
+                .add(createServiceDependency()
+                    .setService(LogService.class)
+                    .setRequired(false)));
+
+
+        manager.add(createService().setInterface(UserAdminConfigurator.class.getName(), null)
+            .setImplementation(userAdminStore)
+            .add(createServiceDependency()
+                .setService(UserAdmin.class)
+                .setAutoConfig(false)
+                .setCallbacks("userAdminAdded", "userAdminRemoved"))
+            .add(createServiceDependency()
+                .setService(LogService.class)
+                .setRequired(false)));
+    }
+
+    @Override
+    public void destroy(BundleContext context, DependencyManager manager) throws Exception {
+        // do nothing
+    }
+}

Added: incubator/ace/trunk/server/src/net/luminis/liq/resourceprocessor/useradmin/impl/Processor.java
URL: http://svn.apache.org/viewvc/incubator/ace/trunk/server/src/net/luminis/liq/resourceprocessor/useradmin/impl/Processor.java?rev=788992&view=auto
==============================================================================
--- incubator/ace/trunk/server/src/net/luminis/liq/resourceprocessor/useradmin/impl/Processor.java (added)
+++ incubator/ace/trunk/server/src/net/luminis/liq/resourceprocessor/useradmin/impl/Processor.java Sat Jun 27 15:53:04 2009
@@ -0,0 +1,137 @@
+package net.luminis.liq.resourceprocessor.useradmin.impl;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.osgi.service.deploymentadmin.spi.DeploymentSession;
+import org.osgi.service.deploymentadmin.spi.ResourceProcessor;
+import org.osgi.service.deploymentadmin.spi.ResourceProcessorException;
+import org.osgi.service.log.LogService;
+
+/**
+ * Implementation of the ResourceProcessor. This base class takes care of
+ * the interaction with the DeploymentAdmin, while delegating the 'actual' work
+ * to a store.
+ */
+public class Processor implements ResourceProcessor{
+    private volatile LogService m_log; /* Injected by dependency manager */
+
+    private volatile DeploymentSession m_session;
+    private String m_deploymentPackageName;
+    private List<String> m_toInstall;
+    private List<String> m_toRemove;
+
+    private ResourceStore m_resourceStore;
+
+    Processor(ResourceStore store) {
+        m_resourceStore = store;
+    }
+
+    /**
+     * Sets up the necessary environment for a deployment session.
+     */
+    private void startSession(DeploymentSession session) {
+        if (m_session != null) {
+            throw new IllegalArgumentException("This resource processor is currently processing another deployment session, installing deploymentpackage" + m_session.getTargetDeploymentPackage().getName());
+        }
+        m_session = session;
+        m_toInstall = new ArrayList<String>();
+        m_toRemove = new ArrayList<String>();
+
+        String fromSource = session.getSourceDeploymentPackage().getName();
+        String fromTarget = session.getTargetDeploymentPackage().getName();
+        if (fromSource.equals("")) {
+            m_deploymentPackageName = fromTarget;
+        }
+        else {
+            m_deploymentPackageName = fromSource;
+        }
+    }
+
+    /**
+     * Ends a deployment session.
+     */
+    private void endSession() {
+        m_session = null;
+        m_deploymentPackageName = null;
+        m_toInstall = null;
+        m_toRemove = null;
+    }
+
+    private void ensureSession()  {
+        if (m_session == null) {
+            throw new IllegalStateException("This resource processor is currently not part of a deployment session.");
+        }
+    }
+
+    public void begin(DeploymentSession session) {
+        startSession(session);
+    }
+
+    public void process(String name, InputStream stream) throws ResourceProcessorException {
+        ensureSession();
+        String originalDeploymentPackage = m_resourceStore.getDeploymentPackage(name);
+        if ((originalDeploymentPackage != null) && !m_deploymentPackageName.equals(originalDeploymentPackage)) {
+            throw new ResourceProcessorException(ResourceProcessorException.CODE_RESOURCE_SHARING_VIOLATION, "Resource " + name + " does not belong to deployment package " + m_deploymentPackageName + ", but to " + originalDeploymentPackage);
+        }
+
+        try {
+            m_resourceStore.addResource(m_deploymentPackageName, name, stream);
+        }
+        catch (IOException e) {
+            throw new ResourceProcessorException(ResourceProcessorException.CODE_OTHER_ERROR, "Error storing resource.", e);
+        }
+
+        m_toInstall.add(name);
+    }
+
+    public void dropped(String resource) throws ResourceProcessorException {
+        ensureSession();
+        m_toRemove.add(resource);
+    }
+
+    public void dropAllResources() throws ResourceProcessorException {
+        ensureSession();
+        m_toRemove.addAll(m_resourceStore.getResources(m_deploymentPackageName));
+    }
+
+    public void prepare() throws ResourceProcessorException {
+        ensureSession();
+    }
+
+    public void commit() {
+        ensureSession();
+        while (!m_toInstall.isEmpty()) {
+            try {
+                m_resourceStore.install(m_toInstall.remove(0));
+            }
+            catch (IOException ioe) {
+                m_log.log(LogService.LOG_ERROR, "Error occurred installing resource", ioe);
+            }
+        }
+        while (!m_toRemove.isEmpty()) {
+            try {
+                m_resourceStore.uninstall(m_toRemove.remove(0));
+            }
+            catch (IOException ioe) {
+                m_log.log(LogService.LOG_ERROR, "Error occurred removing resource", ioe);
+            }
+        }
+
+        endSession();
+    }
+
+    public void rollback() {
+        // nothing special to do.
+        ensureSession();
+        endSession();
+    }
+
+    public void cancel() {
+        ensureSession();
+        // Nothing to do: we have no long-running operation, we only read the stream.
+    }
+
+}

Added: incubator/ace/trunk/server/src/net/luminis/liq/resourceprocessor/useradmin/impl/ResourceStore.java
URL: http://svn.apache.org/viewvc/incubator/ace/trunk/server/src/net/luminis/liq/resourceprocessor/useradmin/impl/ResourceStore.java?rev=788992&view=auto
==============================================================================
--- incubator/ace/trunk/server/src/net/luminis/liq/resourceprocessor/useradmin/impl/ResourceStore.java (added)
+++ incubator/ace/trunk/server/src/net/luminis/liq/resourceprocessor/useradmin/impl/ResourceStore.java Sat Jun 27 15:53:04 2009
@@ -0,0 +1,162 @@
+package net.luminis.liq.resourceprocessor.useradmin.impl;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.osgi.framework.BundleContext;
+import org.osgi.service.deploymentadmin.spi.ResourceProcessorException;
+
+/**
+ * The ResourceStore keeps track of resources, keeps them around in a separate storage
+ * for installing and removing, and keeps track of which resource belongs to which deployment package.
+ * The information stored by the ResourceStore will be persisted as often as possible, to allow
+ * crash recovery.
+ */
+abstract class ResourceStore {
+    private static final int BUFFER_SIZE = 1024;
+
+    private static final String TEMP_DIR = "resources";
+
+    private final BundleContext m_context;
+    Map<String, String> m_resources;
+
+    ResourceStore(BundleContext context) {
+        m_context = context;
+
+        File baseDir = m_context.getDataFile(TEMP_DIR);
+
+        m_resources = new HashMap<String, String>();
+
+        // Fill our resources overview with the data that is available on disk.
+        File[] deploymentPackageList = baseDir.listFiles();
+        if (deploymentPackageList != null) {
+            for (File resourceDirectory : deploymentPackageList) {
+                if (resourceDirectory.isDirectory()) {
+                    String[] fileList = resourceDirectory.list();
+                    if (fileList != null) {
+                        for (String resourceName : fileList) {
+                            m_resources.put(resourceName, resourceDirectory.getName());
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Adds a resource to persistent storage and handles the administration.
+     * @param deploymentPackageName The name of a deployment package.
+     * @param name The name of the resource.
+     * @param stream A stream form which the resource with <code>name</code> can be read.
+     */
+    public void addResource(String deploymentPackageName, String name, InputStream stream) throws IOException, ResourceProcessorException {
+        synchronized (m_resources) {
+            File resourceDirectory = new File(m_context.getDataFile(TEMP_DIR), deploymentPackageName);
+            resourceDirectory.mkdirs();
+
+            File resourceFile = new File(resourceDirectory, name);
+            if (resourceFile.exists()) {
+                resourceFile.delete();
+            }
+
+            FileOutputStream resourceStream = null;
+            try {
+                resourceFile.createNewFile();
+                resourceStream = new FileOutputStream(resourceFile);
+
+                byte[] buf = new byte[BUFFER_SIZE];
+                for (int count = stream.read(buf); count != -1; count = stream.read(buf)) {
+                    resourceStream.write(buf, 0, count);
+                }
+            }
+            finally {
+                if (resourceStream != null) {
+                    try {
+                        resourceStream.close();
+                    }
+                    catch (IOException ioe) {
+                        // nothing to do
+                    }
+                }
+            }
+
+            try {
+                InputStream input = new FileInputStream(resourceFile);
+                validate(input);
+                input.close();
+            }
+            catch (Exception e) {
+                resourceFile.delete();
+                throw new ResourceProcessorException(ResourceProcessorException.CODE_OTHER_ERROR, "Error validating resource.", e);
+            }
+
+            m_resources.put(name, deploymentPackageName);
+        }
+    }
+
+    /**
+     * Checks the validity of a resource.
+     * @param resource A stream containing the resource
+     * @throws Exception Any exception can be thrown from this method, indicating 'what is wrong' with the resource.
+     */
+    public abstract void validate(InputStream resource) throws Exception;
+    /**
+     * Installs a given resource
+     * @param resourceName The name of the resource.
+     */
+    public abstract void install(String resourceName) throws IOException;
+    /**
+     * Uninstalls a given resource
+     * @param resourceName The name of the resource.
+     */
+    public abstract void uninstall(String resourceName) throws IOException;
+
+    /**
+     * Gets the names of all driver bundles that belong to a given deployment package.
+     * @param deploymentPackageName The name of a deployment package.
+     * @return A list of the names of all driver bundles that belong to <code>deploymentPackageName</code>.
+     */
+    public List<String> getResources(String deploymentPackageName) {
+        synchronized (m_resources) {
+            List<String> result = new ArrayList<String>();
+            for (Map.Entry<String, String> entry : m_resources.entrySet()) {
+                if (entry.getValue().equals(deploymentPackageName)) {
+                    result.add(entry.getKey());
+                }
+            }
+            return result;
+        }
+    }
+
+    /**
+     * Gets the name of the deployment package to which a given resource belongs.
+     * @param resourceName The name of a resource.
+     * @return The name of the deployment package to which <code>resourceName</code>
+     * belongs, or <code>null</code> if this resource is unknown.
+     */
+    public String getDeploymentPackage(String resourceName) {
+        return m_resources.get(resourceName);
+    }
+
+    /**
+     * Gets the stream belonging to a given resource.
+     * @param name The name of a the resource.
+     * @return an InputStream providing access to the named resource. It is the caller's
+     * task to close it.
+     * @throws IOException Thrown when an exception occurs accessing the resource.
+     */
+    protected InputStream getResource(String name) throws IOException {
+        File resource = new File(new File(m_context.getDataFile(TEMP_DIR), m_resources.get(name)), name);
+
+        return new FileInputStream(resource);
+    }
+
+
+}

Added: incubator/ace/trunk/server/src/net/luminis/liq/resourceprocessor/useradmin/impl/UserAdminStore.java
URL: http://svn.apache.org/viewvc/incubator/ace/trunk/server/src/net/luminis/liq/resourceprocessor/useradmin/impl/UserAdminStore.java?rev=788992&view=auto
==============================================================================
--- incubator/ace/trunk/server/src/net/luminis/liq/resourceprocessor/useradmin/impl/UserAdminStore.java (added)
+++ incubator/ace/trunk/server/src/net/luminis/liq/resourceprocessor/useradmin/impl/UserAdminStore.java Sat Jun 27 15:53:04 2009
@@ -0,0 +1,390 @@
+package net.luminis.liq.resourceprocessor.useradmin.impl;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Dictionary;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+
+import net.luminis.liq.resourceprocessor.useradmin.UserAdminConfigurator;
+
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.service.log.LogService;
+import org.osgi.service.useradmin.Group;
+import org.osgi.service.useradmin.Role;
+import org.osgi.service.useradmin.User;
+import org.osgi.service.useradmin.UserAdmin;
+import org.w3c.dom.Document;
+import org.w3c.dom.Node;
+import org.xml.sax.SAXException;
+
+public class UserAdminStore extends ResourceStore implements UserAdminConfigurator {
+
+    private UserAdmin m_userAdmin;
+    private Object m_userAdminLock = new Object();
+    private volatile LogService m_log;
+
+    private List<ProcessRole> m_toInstall = new ArrayList<ProcessRole>();
+    private List<ProcessRole> m_toRemove = new ArrayList<ProcessRole>();
+    private Object m_installListLock = new Object();
+    private boolean m_clear;
+
+    UserAdminStore(BundleContext context) {
+        super(context);
+    }
+
+    @Override
+    public void install(String resourceName) throws IOException {
+        InputStream input = getResource(resourceName);
+        install(input);
+    }
+
+    public void install(Document doc) {
+        installRoles(doc);
+        updateUserAdmin();
+    }
+
+    public void install(InputStream input) throws IOException {
+        install(getDocument(input));
+    }
+
+    @Override
+    public void uninstall(String resourceName) throws IOException {
+        InputStream input = getResource(resourceName);
+        uninstall(getDocument(input));
+    }
+
+    public void uninstall(Document doc) {
+        removeRoles(doc);
+        updateUserAdmin();
+    }
+
+    public void uninstall(InputStream input) throws IOException {
+        uninstall(getDocument(input));
+    }
+
+    public void setUsers(Document doc) {
+        m_toInstall.clear();
+        m_toRemove.clear();
+        installRoles(doc);
+        m_clear = true;
+        updateUserAdmin();
+    }
+
+    public void setUsers(InputStream input) throws IOException {
+        setUsers(getDocument(input));
+    }
+
+    @Override
+    public void validate(InputStream resource) throws Exception {
+        Document doc = getDocument(resource);
+        getRoles(doc);
+    }
+
+    /**
+     * Installs the users and groups found in a document.
+     */
+    private void installRoles(Document doc) {
+        synchronized (m_installListLock) {
+            m_toInstall.addAll(getRoles(doc));
+        }
+    }
+
+    /**
+     * Removes the users and groups found in a document.
+     */
+    private void removeRoles(Document doc) {
+        synchronized (m_installListLock) {
+            // do this backwards from the order in the file
+            List<ProcessRole> roles = getRoles(doc);
+            ListIterator<ProcessRole> i = roles.listIterator(roles.size());
+            for (ProcessRole role = i.previous(); i.hasPrevious(); i.previous()) {
+                m_toRemove.add(role);
+            }
+        }
+    }
+
+    /**
+     * Updates the currently present UserAdmin with the data in m_toInstall and m_toRemove.
+     */
+    @SuppressWarnings("unchecked")
+    private void updateUserAdmin() {
+        synchronized(m_installListLock) {
+            synchronized (m_userAdminLock) {
+                if (m_userAdmin == null) {
+                    return;
+                }
+
+                List<String> updated = new ArrayList<String>();
+
+                // install or update all roles we have to update
+                while (!m_toInstall.isEmpty()) {
+                    ProcessRole role = m_toInstall.remove(0);
+                    updateRole(role);
+                    updated.add(role.getName());
+                }
+
+                // remove all roles that have not been updated if this install
+                // is a full install
+                if (m_clear) {
+                    Role[] roles = null;
+                    try {
+                        roles = m_userAdmin.getRoles(null);
+                    }
+                    catch (InvalidSyntaxException e) {
+                        // Will not happen, since we pass in a null filter.
+                    }
+                    for (Role r : roles) {
+                        if (!updated.contains(r.getName())) {
+                            m_userAdmin.removeRole(r.getName());
+                        }
+                    }
+                }
+
+                // if this is not a full install, remove any roles that should be
+                // removed
+                if (!m_clear) {
+                    while (!m_toRemove.isEmpty()) {
+                        ProcessRole role = m_toRemove.remove(m_toRemove.size());
+                        m_userAdmin.removeRole(role.getName());
+                    }
+                }
+
+                m_clear = false;
+            }
+        }
+    }
+
+    /**
+     * Updates a role with new parameter, but reuses the UserAdmin's role object
+     * for this (if available).
+     */
+    @SuppressWarnings("unchecked")
+    private void updateRole(ProcessRole role) {
+        Role r = m_userAdmin.getRole(role.getName());
+        if (r == null) {
+            r = m_userAdmin.createRole(role.getName(), role.getType());
+        }
+        clearDictionary(r.getProperties());
+        for (Entry<String, Object> entry : role.getProperties().entrySet()) {
+            r.getProperties().put(entry.getKey(), entry.getValue());
+        }
+        clearDictionary(((User) r).getCredentials());
+        if (role.getType() == Role.USER) {
+            for (Entry<String, Object> entry : role.getCredentials().entrySet()) {
+                ((User) r).getCredentials().put(entry.getKey(), entry.getValue());
+            }
+        }
+        for (Group g : memberOf(r)) {
+            g.removeMember(r);
+        }
+        for (String groupName : role.getMemberOf()) {
+            Group g = (Group) m_userAdmin.getRole(groupName);
+            if (g == null) {
+                m_log.log(LogService.LOG_WARNING, "Cannot add user " + role.getName() + " to group " + groupName + ", because the group does not exist.");
+                continue;
+            }
+            g.addMember(r);
+        }
+    }
+
+    private void clearDictionary(Dictionary dict) {
+        Enumeration i = dict.keys();
+        while (i.hasMoreElements()) {
+            dict.remove(i.nextElement());
+        }
+    }
+
+    /**
+     * Helper that finds all groups this role is a member of.
+     */
+    private Group[] memberOf(Role r) {
+        List<Group> result = new ArrayList<Group>();
+        Role[] roles = null;
+        try {
+            roles = m_userAdmin.getRoles(null);
+        }
+        catch (InvalidSyntaxException e) {
+            // Will not happen, since we pass in a null filter.
+        }
+        if (roles == null) {
+            return new Group[0];
+        }
+        for (Role group : roles) {
+            if (group instanceof Group) {
+                Role[] members = ((Group) group).getMembers();
+                if (members != null) {
+                    if (contains(r, members)) {
+                        result.add((Group) group);
+                    }
+                }
+            }
+        }
+
+        return result.toArray(new Group[result.size()]);
+    }
+
+    /**
+     * Helper method that checks the presence of an object in an array. Returns
+     * <code>true</code> if <code>t</code> is in <code>ts</code>, <code>false</code> otherwise.
+     */
+    private <T> boolean contains(T t, T[] ts) {
+        for (T current : ts) {
+            if (current.equals(t)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Called by the dependency manager when a user admin becomes available.
+     */
+    public void userAdminAdded(UserAdmin admin) {
+        synchronized (m_userAdminLock) {
+            if (m_userAdmin != null) {
+                throw new IllegalStateException("UserAdminStore is intended to work with a single user admin.");
+            }
+            m_userAdmin = admin;
+            updateUserAdmin();
+        }
+    }
+
+    /**
+     * Called by the dependency manager when a user admin goes away.
+     */
+    public void userAdminRemoved(UserAdmin admin) {
+        synchronized (m_userAdminLock) {
+            if (m_userAdmin != admin) {
+                throw new IllegalStateException("UserAdminStore is intended to work with a single user admin.");
+            }
+            m_userAdmin = null;
+        }
+    }
+
+    /**
+     * Gets the DOM document contained in a stream.
+     */
+    private Document getDocument(InputStream input) throws IOException {
+        try {
+            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+            DocumentBuilder builder;
+            builder = factory.newDocumentBuilder();
+            return builder.parse(input);
+        }
+        catch (ParserConfigurationException e) {
+            throw new IOException("Error instantiation XML parser:" + e.getMessage());
+        }
+        catch (SAXException e) {
+            throw new IOException("Error parsing user data:" + e.getMessage());
+        }
+    }
+
+    /**
+     * Gets all roles that are present in a document.
+     * @param doc The document to use.
+     * @return A list of ProcessRoles.
+     */
+    private List<ProcessRole> getRoles(Document doc) {
+        List<ProcessRole> result = new ArrayList<ProcessRole>();
+        for (Node node = doc.getFirstChild().getFirstChild(); node != null; node = node.getNextSibling()) {
+            if (!node.getNodeName().equals("#text")) {
+                result.add(getRole(node));
+            }
+        }
+        return result;
+    }
+
+    /**
+     * Helper method that takes a single XML node containing a 'user' or 'group', and return a
+     * ProcessRole for it.
+     */
+    private ProcessRole getRole(Node node) {
+        ProcessRole result = new ProcessRole(node.getAttributes().getNamedItem("name").getTextContent(), (node.getNodeName().equals("group") ? Role.GROUP : Role.USER));
+
+        for (Node child = node.getFirstChild(); child != null; child = child.getNextSibling()) {
+            if (child.getNodeName().equals("properties")) {
+                for (Node property = child.getFirstChild(); property != null; property = property.getNextSibling()) {
+                    if (!property.getNodeName().equals("#text")) {
+                        String type = null;
+                        Node typeNode = property.getAttributes().getNamedItem("type");
+                        if (typeNode != null) {
+                            type = typeNode.getTextContent();
+                        }
+                        result.getProperties().put(property.getNodeName(), "byte[]".equals(type) ? property.getTextContent().getBytes() : property.getTextContent());
+                    }
+                }
+            }
+            else if (child.getNodeName().equals("credentials")) {
+                for (Node credential = child.getFirstChild(); credential != null; credential = credential.getNextSibling()) {
+                    if (!credential.getNodeName().equals("#text")) {
+                        String type = null;
+                        Node typeNode = credential.getAttributes().getNamedItem("type");
+                        if (typeNode != null) {
+                            type = typeNode.getTextContent();
+                        }
+                        result.getCredentials().put(credential.getNodeName(), "byte[]".equals(type) ? credential.getTextContent().getBytes() : credential.getTextContent());
+                    }
+
+                }
+            }
+            else if (child.getNodeName().equals("memberof")) {
+                if (!child.getNodeName().equals("#text")) {
+                    result.getMemberOf().add(child.getTextContent());
+                }
+            }
+        }
+
+        return result;
+    }
+
+    /**
+     * Value object for relaying user information between XML-processing methods and
+     * UserAdmin users.
+     * This indirection is necessary because we want to separate the parsing of the
+     * XML, and the actual installation.
+     */
+    private class ProcessRole {
+        private final int m_type;
+        private final String m_name;
+        private final Map<String, Object> m_properties = new HashMap<String, Object>();
+        private final Map<String, Object> m_credentials = new HashMap<String, Object>();
+        private final List<String> m_memberOf = new ArrayList<String>();
+
+        ProcessRole(String name, int type) {
+            m_name = name;
+            m_type = type;
+        }
+
+        public int getType() {
+            return m_type;
+        }
+
+        public String getName() {
+            return m_name;
+        }
+
+        public Map<String, Object> getProperties() {
+            return m_properties;
+        }
+
+        public Map<String, Object> getCredentials() {
+            return m_credentials;
+        }
+
+        public List<String> getMemberOf() {
+            return m_memberOf;
+        }
+    }
+
+}

Added: incubator/ace/trunk/server/src/net/luminis/liq/server/action/Action.java
URL: http://svn.apache.org/viewvc/incubator/ace/trunk/server/src/net/luminis/liq/server/action/Action.java?rev=788992&view=auto
==============================================================================
--- incubator/ace/trunk/server/src/net/luminis/liq/server/action/Action.java (added)
+++ incubator/ace/trunk/server/src/net/luminis/liq/server/action/Action.java Sat Jun 27 15:53:04 2009
@@ -0,0 +1,21 @@
+package net.luminis.liq.server.action;
+
+import org.osgi.service.event.Event;
+
+/**
+ * An Action is a rather general action which can be used to handle some event.
+ * Action services are to be published with a service property {@link Action#ACTION_NAME},
+ * which states the name of this action, so other services can find it.<br>
+ * <br>
+ * Implementers of Action should specify in their interface which properties should
+ * be available in the event, for the action to be able to do its job.
+ */
+public interface Action {
+    public static final String ACTION_NAME = Action.class.getName() + ".name";
+    
+    /**
+     * Handles an event, performing the main action of this Action.
+     */
+    public void handle(Event event);
+
+}

Added: incubator/ace/trunk/server/src/net/luminis/liq/server/action/MessageAction.java
URL: http://svn.apache.org/viewvc/incubator/ace/trunk/server/src/net/luminis/liq/server/action/MessageAction.java?rev=788992&view=auto
==============================================================================
--- incubator/ace/trunk/server/src/net/luminis/liq/server/action/MessageAction.java (added)
+++ incubator/ace/trunk/server/src/net/luminis/liq/server/action/MessageAction.java Sat Jun 27 15:53:04 2009
@@ -0,0 +1,22 @@
+package net.luminis.liq.server.action;
+
+/**
+ * MessageAction is an action that sends out messages. To do so, it needs a user, and
+ * both a short and a long description. Is it up to the implementation to do something with
+ * it, e.g. use the short description and a subject for an email message, or use the short
+ * description as the text in an SMS.
+ */
+public interface MessageAction extends Action {
+    /**
+     * Key for the event properties containing a User.
+     */
+    public static final String USER = "user";
+    /**
+     * Key for the event properties containing a description as a String.
+     */
+    public static final String DESCRIPTION = "description";
+    /**
+     * Key for the event properties containing a very short description as a String.
+     */
+    public static final String SHORT_DESCRIPTION = "shortDescription";
+}



Mime
View raw message