view artifact-database/src/main/java/de/intevation/artifactdatabase/ArtifactDatabaseImpl.java @ 264:fa0d9acea897

flys/issue65: Added Jetty HTTP server as a replacement option to foster better scalability. Needs testing. artifacts/trunk@1975 c6561f87-3c4e-4783-a992-168aeb5c3f6f
author Sascha L. Teichmann <sascha.teichmann@intevation.de>
date Sun, 22 May 2011 12:40:32 +0000
parents 6cf9560bd249
children 4edaf3073109
line wrap: on
line source
/*
 * Copyright (c) 2010 by Intevation GmbH
 *
 * This program is free software under the LGPL (>=v2.1)
 * Read the file LGPL.txt coming with the software for details
 * or visit http://www.gnu.org/licenses/ if it does not exist.
 */

package de.intevation.artifactdatabase;

import de.intevation.artifacts.common.utils.XMLUtils;

import de.intevation.artifactdatabase.Backend.PersistentArtifact;

import de.intevation.artifacts.Artifact;
import de.intevation.artifacts.ArtifactCollection;
import de.intevation.artifacts.ArtifactCollectionFactory;
import de.intevation.artifacts.ArtifactDatabase;
import de.intevation.artifacts.ArtifactDatabaseException;
import de.intevation.artifacts.ArtifactFactory;
import de.intevation.artifacts.ArtifactNamespaceContext;
import de.intevation.artifacts.ArtifactSerializer;
import de.intevation.artifacts.CallContext;
import de.intevation.artifacts.CallMeta;
import de.intevation.artifacts.CollectionItem;
import de.intevation.artifacts.Service;
import de.intevation.artifacts.ServiceFactory;
import de.intevation.artifacts.User;
import de.intevation.artifacts.UserFactory;

import java.io.IOException;
import java.io.OutputStream;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;

import javax.xml.xpath.XPathConstants;

import org.apache.commons.codec.binary.Base64;
import org.apache.commons.codec.binary.Hex;

import org.apache.log4j.Logger;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;

/**
 * The core implementation of artifact database. This layer exposes
 * the needed methods to the artifact runtime system which e.g. may
 * expose them via REST. The concrete persistent representation of the
 * artifacts is handled by the {@link Backend backend}.
 * @author <a href="mailto:sascha.teichmann@intevation.de">Sascha L. Teichmann</a>
 */
public class ArtifactDatabaseImpl
implements   ArtifactDatabase,
             DatabaseCleaner.LockedIdsProvider,
             Backend.FactoryLookup
{
    private static Logger logger =
        Logger.getLogger(ArtifactDatabaseImpl.class);

    /** Message that is returned if an operation was successful.*/
    public static final String OPERATION_SUCCESSFUL =
        "SUCCESS";

    /** Message that is returned if an operation failed.*/
    public static final String OPERATION_FAILURE =
        "FAILURE";

    /**
     * Error message issued if a requested artifact factory
     * is not registered to this database.
     */
    public static final String NO_SUCH_FACTORY =
        "No such factory";

    /**
     * Error message issued if a requested artifact is not found
     * in this database.
     */
    public static final String NO_SUCH_ARTIFACT =
        "No such artifact";

    /**
     * Error message issued if a requested artifact is not found
     * in this database.
     */
    public static final String NO_SUCH_COLLECTION =
        "No such collection";

    /**
     * Error message issued if the creation of an artifact failed.
     */
    public static final String CREATION_FAILED =
        "Creation of artifact failed";

    /**
     * Error message if an severe internal error occurred.
     */
    public static final String INTERNAL_ERROR =
        "Creation of artifact failed";

    /**
     * Error message issued if a requested service is not
     * offered by this database.
     */
    public static final String NO_SUCH_SERVICE =
        "No such service";

    /**
     * Default digest hash to be used while im-/exporting artifacts.
     */
    public static final String DIGEST_ALGORITHM =
        "SHA-1";

    /**
     * XPath to get the checksum from an XML representation of
     * an exported artifact.
     */
    public static final String XPATH_IMPORT_CHECKSUM =
        "/art:action/art:data/@checksum";

    /**
     * XPath to get the name of the factory which should be
     * used to revive an antrifact that is going to be imported.
     */
    public static final String XPATH_IMPORT_FACTORY =
        "/art:action/art:data/@factory";

    /**
     * XPath to get the base64 encoded data of an artifact
     * that is going to be imported.
     */
    public static final String XPATH_IMPORT_DATA =
        "/art:action/art:data/text()";

    /**
     * Error message issued if the checksum of an
     * artifact to be imported has an invalid syntax.
     */
    public static final String INVALID_CHECKSUM =
        "Invalid checksum";

    /**
     * Error message issued the checksum validation
     * of an artifact to be imported fails.
     */
    public static final String CHECKSUM_MISMATCH =
        "Mismatching checksum";

    /**
     * Error message issued if an artifact to be imported
     * does not have any data.
     */
    public static final String NO_DATA =
        "No data";

    /**
     * Error message issued if the deserialization of
     * an artifact to be imported fails.
     */
    public static final String INVALID_ARTIFACT =
        "Invalid artifact";


    // User constants

    /**
     * Error message issued if the creation of a user failed.
     */
    public static final String USER_CREATION_FAILED =
        "Creation of user failed.";

    /** XPath to figure out the name of a new user.*/
    public static final String XPATH_USERNAME =
        "/art:action/art:user/@name";

    /** XPath to figure out the role of a new user.*/
    public static final String XPATH_USERROLE =
        "/art:action/art:user/art:role";

    /** Error message if a specified user does not exist.*/
    public static final String NO_SUCH_USER =
        "No such user";

    /** Error message if no username is given for user creation.*/
    public static final String NO_USERNAME =
        "Invalid username";

    // Collection constants

    /**
     * Error message issued if the creation of a collection failed.
     */
    public static final String COLLECTION_CREATION_FAILED =
        "Creation of collection failed";

    /**
     * XPath to figure out the name of a collection described in the incoming
     * document.
     */
    public static final String XPATH_COLLECTION_NAME =
        "/art:action/art:type/art:collection/@name";

    /**
     * XPath to figure out the attributes for a collection.
     */
    public static final String XPATH_COLLECTION_ATTRIBUTE =
        "/art:action/art:type/art:collection/art:attribute";

    /**
     * XPath to figure out the attributes for an artifact that is put into a
     * collection.
     */
    public static final String XPATH_COLLECTION_ITEM_ATTRIBUTE =
        "/art:action/art:type/art:artifact/art:attribute";


    /**
     * This inner class allows the deferral of writing the output
     * of the artifact's out() call.
     */
    public class DeferredOutputImpl
    implements   DeferredOutput
    {
        /**
         * The persistence wrapper around a living artifact.
         */
        protected PersistentArtifact artifact;
        /**
         * The input document for the artifact's out() call.
         */
        protected Document           format;
        /**
         * The meta information of the artifact's out() call.
         */
        protected CallMeta           callMeta;

        /**
         * Default constructor.
         */
        public DeferredOutputImpl() {
        }

        /**
         * Constructor to create a deferred execution unit for
         * the artifact's out() call given an artifact, an input document
         * an the meta information.
         * @param artifact The persistence wrapper around a living artifact.
         * @param format   The input document for the artifact's out() call.
         * @param callMeta The meta information of the artifact's out() call.
         */
        public DeferredOutputImpl(
            PersistentArtifact artifact,
            Document           format,
            CallMeta           callMeta
        ) {
            this.artifact = artifact;
            this.format   = format;
            this.callMeta = callMeta;
        }

        public void write(OutputStream output) throws IOException {

            ArtifactCallContext cc = new ArtifactCallContext(
                ArtifactDatabaseImpl.this,
                CallContext.TOUCH,
                callMeta,
                artifact);

            try {
                artifact.getArtifact().out(format, output, cc);
            }
            finally {
                cc.postCall();
            }
        }
    } // class DeferredOutputImpl


    /**
     * This inner class allows the deferral of writing the output
     * of the artifact's out() call.
     */
    public class DeferredCollectionOutputImpl
    implements   DeferredOutput
    {
        /**
         * The persistence wrapper around a living collection.
         */
        protected ArtifactCollection collection;
        /**
         * The input document for the collection's out() call.
         */
        protected Document format;
        /**
         * The meta information of the collection's out() call.
         */
        protected CallMeta callMeta;

        /**
         * Default constructor.
         */
        public DeferredCollectionOutputImpl() {
        }

        /**
         * Constructor to create a deferred execution unit for
         * the collection's out() call given a collection, an input document
         * an the meta information.
         * @param collection The collection.
         * @param format   The input document for the collection's out() call.
         * @param callMeta The meta information of the collection's out() call.
         */
        public DeferredCollectionOutputImpl(
            ArtifactCollection collection,
            Document           format,
            CallMeta           callMeta
        ) {
            this.collection = collection;
            this.format     = format;
            this.callMeta   = callMeta;
        }

        public void write(OutputStream output) throws IOException {

            CollectionCallContext cc = new CollectionCallContext(
                ArtifactDatabaseImpl.this,
                CallContext.TOUCH,
                callMeta,
                collection);

            try {
                collection.out(format, output, cc);
            }
            finally {
                cc.postCall();
            }
        }
    } // class DeferredCollectionOutputImpl

    /**
     * List of name/description pairs needed for
     * {@link #artifactFactoryNamesAndDescriptions() }.
     */
    protected String [][] factoryNamesAndDescription;
    /**
     * Map to access artifact factories by there name.
     */
    protected HashMap     name2factory;

    /**
     * List of name/description pairs needed for
     * {@link #serviceNamesAndDescriptions() }.
     */
    protected String [][] serviceNamesAndDescription;
    /**
     * Map to access services by there name.
     */
    protected HashMap     name2service;

    /**
     * The factory that is used to create new artifact collections.
     */
    protected ArtifactCollectionFactory collectionFactory;

    /**
     * The factory that is used to create and list users.
     */
    protected UserFactory userFactory;

    /**
     * Reference to the storage backend.
     */
    protected Backend     backend;
    /**
     * Reference of the global context of the artifact runtime system.
     */
    protected Object      context;

    /**
     * The signing secret to be used for ex-/importing artifacts.
     */
    protected byte []     exportSecret;

    /**
     * A set of ids of artifact which currently running in background.
     * This artifacts should not be removed from the database by the
     * database cleaner.
     */
    protected HashSet<Integer>  backgroundIds;

    protected CallContext.Listener callContextListener;

    /**
     * Default constructor.
     */
    public ArtifactDatabaseImpl() {
    }

    /**
     * Constructor to create a artifact database with the given
     * bootstrap parameters like artifact- and service factories et. al.
     * Created this way the artifact database has no backend.
     * @param bootstrap The parameters to start this artifact database.
     */
    public ArtifactDatabaseImpl(FactoryBootstrap bootstrap) {
        this(bootstrap, null);
    }

    /**
     * Constructor to create a artifact database with the a given
     * backend and
     * bootstrap parameters like artifact- and service factories et. al.
     * @param bootstrap The parameters to start this artifact database.
     * @param backend   The storage backend.
     */
    public ArtifactDatabaseImpl(FactoryBootstrap bootstrap, Backend backend) {

        backgroundIds = new HashSet<Integer>();

        setupArtifactCollectionFactory(bootstrap);
        setupArtifactFactories(bootstrap);
        setupServices(bootstrap);
        setupUserFactory(bootstrap);
        setupCallContextListener(bootstrap);

        context      = bootstrap.getContext();
        exportSecret = bootstrap.getExportSecret();

        wireWithBackend(backend);
    }

    public CallContext.Listener getCallContextListener() {
        return callContextListener;
    }

    public void setCallContextListener(
        CallContext.Listener callContextListener
    ) {
        this.callContextListener = callContextListener;
    }

    /**
     * Used to extract the artifact collection factory from bootstrap.
     *
     * @param bootstrap The bootstrap parameters.
     */
    protected void setupArtifactCollectionFactory(FactoryBootstrap bootstrap) {
        collectionFactory = bootstrap.getArtifactCollectionFactory();
    }

    /**
     * Used to extract the artifact factories from the bootstrap
     * parameters and building the internal lookup tables.
     * @param bootstrap The bootstrap parameters.
     */
    protected void setupArtifactFactories(FactoryBootstrap bootstrap) {
        name2factory  = new HashMap();

        ArtifactFactory [] factories = bootstrap.getArtifactFactories();
        factoryNamesAndDescription = new String[factories.length][];

        for (int i = 0; i < factories.length; ++i) {

            ArtifactFactory factory = factories[i];

            String name        = factory.getName();
            String description = factory.getDescription();

            factoryNamesAndDescription[i] =
                new String [] { name, description };

            name2factory.put(name, factory);
        }
    }

    /**
     * Used to extract the callContextListener from the bootstrap.
     *
     * @param bootstrap The bootstrap parameters.
     */
    protected void setupCallContextListener(FactoryBootstrap bootstrap) {
        setCallContextListener(bootstrap.getCallContextListener());
    }

    /**
     * Used to extract the user factory from the bootstrap.
     */
    protected void setupUserFactory(FactoryBootstrap bootstrap) {
        userFactory = bootstrap.getUserFactory();
    }

    /**
     * Used to extract the service factories from the bootstrap
     * parameters, setting up the services and building the internal
     * lookup tables.
     * @param bootstrap The bootstrap parameters.
     */
    protected void setupServices(FactoryBootstrap bootstrap) {

        name2service  = new HashMap();

        ServiceFactory [] serviceFactories =
            bootstrap.getServiceFactories();

        serviceNamesAndDescription =
            new String[serviceFactories.length][];

        for (int i = 0; i < serviceFactories.length; ++i) {
            ServiceFactory factory = serviceFactories[i];

            String name        = factory.getName();
            String description = factory.getDescription();

            serviceNamesAndDescription[i] =
                new String [] { name, description };

            name2service.put(
                name,
                factory.createService(bootstrap.getContext()));
        }

    }

    /**
     * Wires a storage backend to this artifact database and
     * establishes a callback to be able to revive artifacts
     * via the serializers of this artifact factories.
     * @param backend The backend to be wired with this artifact database.
     */
    public void wireWithBackend(Backend backend) {
        if (backend != null) {
            this.backend = backend;
            backend.setFactoryLookup(this);
        }
    }

    /**
     * Called after an backgrounded artifact signals its
     * will to be written back to the backend.
     * @param artifact The persistence wrapper around
     * the backgrounded artifact.
     * @param action The action to be performed.
     */
    protected void fromBackground(PersistentArtifact artifact, int action) {
        logger.warn("BACKGROUND processing is not fully implemented, yet!");
        switch (action) {
            case CallContext.NOTHING:
                break;
            case CallContext.TOUCH:
                artifact.touch();
                break;
            case CallContext.STORE:
                artifact.store();
                break;
            default:
                logger.warn("operation not allowed in fromBackground");
        }
        removeIdFromBackground(artifact.getId());
    }

    /**
     * Removes an artifact's database id from the set of backgrounded
     * artifacts. The database cleaner is now able to remove it safely
     * from the database again.
     * @param id The database id of the artifact.
     */
    protected void removeIdFromBackground(int id) {
        synchronized (backgroundIds) {
            backgroundIds.remove(id);
        }
    }

    /**
     * Adds an artifact's database id to the set of artifacts
     * running in backgroound. To be in this set prevents the
     * artifact to be removed from the database by the database cleaner.
     * @param id The database id of the artifact to be protected
     * from being removed from the database.
     */
    protected void addIdToBackground(int id) {
        synchronized (backgroundIds) {
            backgroundIds.add(Integer.valueOf(id));
        }
    }

    public Set<Integer> getLockedIds() {
        synchronized (backgroundIds) {
            return new HashSet<Integer>(backgroundIds);
        }
    }

    public String [][] artifactFactoryNamesAndDescriptions() {
        return factoryNamesAndDescription;
    }

    public ArtifactFactory getInternalArtifactFactory(String factoryName) {
        return getArtifactFactory(factoryName);
    }

    public ArtifactFactory getArtifactFactory(String factoryName) {
        return (ArtifactFactory)name2factory.get(factoryName);
    }

    public UserFactory getUserFactory() {
        return userFactory;
    }

    public ArtifactCollectionFactory getArtifactCollectionFactory() {
        return collectionFactory;
    }

    public Document createArtifactWithFactory(
        String   factoryName,
        CallMeta callMeta,
        Document data
    )
    throws ArtifactDatabaseException
    {
        ArtifactFactory factory = getArtifactFactory(factoryName);

        if (factory == null) {
            throw new ArtifactDatabaseException(NO_SUCH_FACTORY);
        }

        Artifact artifact = factory.createArtifact(
            backend.newIdentifier(),
            context,
            data);

        if (artifact == null) {
            throw new ArtifactDatabaseException(CREATION_FAILED);
        }

        PersistentArtifact persistentArtifact;

        try {
            persistentArtifact = backend.storeInitially(
                artifact,
                factory,
                factory.timeToLiveUntouched(artifact, context));
        }
        catch (Exception e) {
            logger.error(e.getLocalizedMessage(), e);
            throw new ArtifactDatabaseException(CREATION_FAILED);
        }

        ArtifactCallContext cc = new ArtifactCallContext(
            ArtifactDatabaseImpl.this,
            CallContext.NOTHING,
            callMeta,
            persistentArtifact);

        try {
            return artifact.describe(null, cc);
        }
        finally {
            cc.postCall();
        }
    }

    public Document describe(
        String   identifier,
        Document data,
        CallMeta callMeta
    )
    throws ArtifactDatabaseException
    {
        // TODO: Handle background tasks
        PersistentArtifact artifact = backend.getArtifact(identifier);

        if (artifact == null) {
            throw new ArtifactDatabaseException(NO_SUCH_ARTIFACT);
        }

        ArtifactCallContext cc = new ArtifactCallContext(
            ArtifactDatabaseImpl.this,
            CallContext.TOUCH,
            callMeta,
            artifact);

        try {
            return artifact.getArtifact().describe(data, cc);
        }
        finally {
            cc.postCall();
        }
    }

    public Document advance(
        String   identifier,
        Document target,
        CallMeta callMeta
    )
    throws ArtifactDatabaseException
    {
        // TODO: Handle background tasks
        PersistentArtifact artifact = backend.getArtifact(identifier);

        if (artifact == null) {
            throw new ArtifactDatabaseException(NO_SUCH_ARTIFACT);
        }

        ArtifactCallContext cc = new ArtifactCallContext(
            ArtifactDatabaseImpl.this,
            CallContext.STORE,
            callMeta,
            artifact);

        try {
            return artifact.getArtifact().advance(target, cc);
        }
        finally {
            cc.postCall();
        }
    }

    public Document feed(String identifier, Document data, CallMeta callMeta)
        throws ArtifactDatabaseException
    {
        // TODO: Handle background tasks
        PersistentArtifact artifact = backend.getArtifact(identifier);

        if (artifact == null) {
            throw new ArtifactDatabaseException(NO_SUCH_ARTIFACT);
        }

        ArtifactCallContext cc = new ArtifactCallContext(
            ArtifactDatabaseImpl.this,
            CallContext.STORE,
            callMeta,
            artifact);

        try {
            return artifact.getArtifact().feed(data, cc);
        }
        finally {
            cc.postCall();
        }
    }

    public DeferredOutput out(
        String   identifier,
        Document format,
        CallMeta callMeta
    )
    throws ArtifactDatabaseException
    {
        // TODO: Handle background tasks
        PersistentArtifact artifact = backend.getArtifact(identifier);

        if (artifact == null) {
            throw new ArtifactDatabaseException(NO_SUCH_ARTIFACT);
        }

        return new DeferredOutputImpl(artifact, format, callMeta);
    }

    public Document exportArtifact(String artifact, CallMeta callMeta)
        throws ArtifactDatabaseException
    {
        final String [] factoryName = new String[1];

        byte [] bytes = (byte [])backend.loadArtifact(
            artifact,
            new Backend.ArtifactLoader() {
                public Object load(
                    ArtifactFactory factory,
                    Long            ttl,
                    byte []         bytes,
                    int             id
                ) {
                    factoryName[0] = factory.getName();

                    ArtifactSerializer serializer = factory.getSerializer();

                    Artifact artifact = serializer.fromBytes(bytes);
                    artifact.cleanup(context);

                    return serializer.toBytes(artifact);
                }
            });

        if (bytes == null) {
            throw new ArtifactDatabaseException(NO_SUCH_ARTIFACT);
        }

        return createExportDocument(
            factoryName[0],
            bytes,
            exportSecret);
    }

    /**
     * Creates an exteral XML representation of an artifact.
     * @param factoryName The name of the factory which is responsible
     * for the serialized artifact.
     * @param artifact The byte data of the artifact itself.
     * @param secret   The signing secret.
     * @return An XML document containing the external representation
     * of the artifact.
     */
    protected static Document createExportDocument(
        String  factoryName,
        byte [] artifact,
        byte [] secret
    ) {
        Document document = XMLUtils.newDocument();

        MessageDigest md;
        try {
            md = MessageDigest.getInstance(DIGEST_ALGORITHM);
        }
        catch (NoSuchAlgorithmException nsae) {
            logger.error(nsae.getLocalizedMessage(), nsae);
            return document;
        }

        md.update(artifact);
        md.update(secret);

        String checksum = Hex.encodeHexString(md.digest());

        XMLUtils.ElementCreator ec = new XMLUtils.ElementCreator(
            document,
            ArtifactNamespaceContext.NAMESPACE_URI,
            ArtifactNamespaceContext.NAMESPACE_PREFIX);

        Element root = ec.create("action");
        document.appendChild(root);

        Element type = ec.create("type");
        ec.addAttr(type, "name", "export", true);
        root.appendChild(type);

        Element data = ec.create("data");
        ec.addAttr(data, "checksum", checksum, true);
        ec.addAttr(data, "factory",  factoryName, true);
        data.setTextContent(Base64.encodeBase64String(artifact));

        root.appendChild(data);

        return document;
    }

    public Document importArtifact(Document input, CallMeta callMeta)
        throws ArtifactDatabaseException
    {
        String factoryName = XMLUtils.xpathString(
            input,
            XPATH_IMPORT_FACTORY,
            ArtifactNamespaceContext.INSTANCE);

        ArtifactFactory factory;

        if (factoryName == null
        || (factoryName = factoryName.trim()).length() == 0
        || (factory = getArtifactFactory(factoryName)) == null) {
            throw new ArtifactDatabaseException(NO_SUCH_FACTORY);
        }

        String checksumString = XMLUtils.xpathString(
            input,
            XPATH_IMPORT_CHECKSUM,
            ArtifactNamespaceContext.INSTANCE);

        byte [] checksum;

        if (checksumString == null
        || (checksumString = checksumString.trim()).length() == 0
        || (checksum = StringUtils.decodeHex(checksumString)) == null
        ) {
            throw new ArtifactDatabaseException(INVALID_CHECKSUM);
        }

        checksumString = null;

        String dataString = XMLUtils.xpathString(
            input,
            XPATH_IMPORT_DATA,
            ArtifactNamespaceContext.INSTANCE);

        if (dataString == null
        || (dataString = dataString.trim()).length() == 0) {
            throw new ArtifactDatabaseException(NO_DATA);
        }

        byte [] data = Base64.decodeBase64(dataString);

        dataString = null;

        MessageDigest md;
        try {
            md = MessageDigest.getInstance(DIGEST_ALGORITHM);
        }
        catch (NoSuchAlgorithmException nsae) {
            logger.error(nsae.getLocalizedMessage(), nsae);
            return XMLUtils.newDocument();
        }

        md.update(data);
        md.update(exportSecret);

        byte [] digest = md.digest();

        if (!Arrays.equals(checksum, digest)) {
            throw new ArtifactDatabaseException(CHECKSUM_MISMATCH);
        }

        ArtifactSerializer serializer = factory.getSerializer();

        Artifact artifact = serializer.fromBytes(data); data = null;

        if (artifact == null) {
            throw new ArtifactDatabaseException(INVALID_ARTIFACT);
        }

        artifact.setIdentifier(backend.newIdentifier());
        PersistentArtifact persistentArtifact;

        try {
            persistentArtifact = backend.storeOrReplace(
                artifact,
                factory,
                factory.timeToLiveUntouched(artifact, context));
        }
        catch (Exception e) {
            logger.error(e.getLocalizedMessage(), e);
            throw new ArtifactDatabaseException(CREATION_FAILED);
        }

        ArtifactCallContext cc = new ArtifactCallContext(
            ArtifactDatabaseImpl.this,
            CallContext.NOTHING,
            callMeta,
            persistentArtifact);

        try {
            return artifact.describe(input, cc);
        }
        finally {
            cc.postCall();
        }
    }

    public String [][] serviceNamesAndDescriptions() {
        return serviceNamesAndDescription;
    }

    public Document process(
        String   serviceName,
        Document input,
        CallMeta callMeta
    )
    throws ArtifactDatabaseException
    {
        Service service = (Service)name2service.get(serviceName);

        if (service == null) {
            throw new ArtifactDatabaseException(NO_SUCH_SERVICE);
        }

        return service.process(input, context, callMeta);
    }

    // User API

    public Document listUsers(CallMeta callMeta)
        throws ArtifactDatabaseException
    {
        UserFactory factory = getUserFactory();

        if (factory == null) {
            throw new ArtifactDatabaseException(NO_SUCH_FACTORY);
        }

        User [] users = backend.getUsers(factory, context);

        if (users != null) {
            logger.debug(users.length + " users found in the backend.");
        }

        Document result = XMLUtils.newDocument();

        XMLUtils.ElementCreator ec = new XMLUtils.ElementCreator(
            result,
            ArtifactNamespaceContext.NAMESPACE_URI,
            ArtifactNamespaceContext.NAMESPACE_PREFIX);

        Element root = ec.create("users");
        result.appendChild(root);

        for (User user: users) {
            Element ue = ec.create("user");
            ec.addAttr(ue, "uuid", user.identifier(), true);
            ec.addAttr(ue, "name", user.getName(), true);

            Document role = user.getRole();

            if (role != null) {
                ue.appendChild(result.importNode(role.getFirstChild(), true));
            }

            root.appendChild(ue);

        }

        return result;
    }

    public Document createUser(Document data, CallMeta callMeta)
        throws ArtifactDatabaseException
    {
        UserFactory factory = getUserFactory();

        if (factory == null) {
            throw new ArtifactDatabaseException(NO_SUCH_FACTORY);
        }

        String name = XMLUtils.xpathString(
            data, XPATH_USERNAME, ArtifactNamespaceContext.INSTANCE);

        if (name == null || name.length() == 0) {
            logger.warn("User without username not accepted!");
            throw new ArtifactDatabaseException(NO_USERNAME);
        }

        Node tmp = (Node) XMLUtils.xpath(
            data,
            XPATH_USERROLE,
            XPathConstants.NODE,
            ArtifactNamespaceContext.INSTANCE);

        Document role = XMLUtils.newDocument();

        if (tmp != null) {
            Node    clone = role.importNode(tmp, true);
            role.appendChild(clone);
        }

        User newUser = null;

        try {
            newUser = backend.createUser(name, role, userFactory, context);
        }
        catch (Exception e) {
            logger.error(e.getMessage(), e);
            throw new ArtifactDatabaseException(USER_CREATION_FAILED);
        }

        Document result = XMLUtils.newDocument();

        XMLUtils.ElementCreator ec = new XMLUtils.ElementCreator(
            result,
            ArtifactNamespaceContext.NAMESPACE_URI,
            ArtifactNamespaceContext.NAMESPACE_PREFIX);

        Element root = ec.create("result");

        if (newUser != null) {
            root.setTextContent(OPERATION_SUCCESSFUL);
        }
        else {
            root.setTextContent(OPERATION_FAILURE);
        }

        result.appendChild(root);

        return result;
    }

    public Document deleteUser(String userId, CallMeta callMeta)
        throws ArtifactDatabaseException
    {
        logger.debug("Delete user: " + userId);

        Document result = XMLUtils.newDocument();

        XMLUtils.ElementCreator ec = new XMLUtils.ElementCreator(
            result,
            ArtifactNamespaceContext.NAMESPACE_URI,
            ArtifactNamespaceContext.NAMESPACE_PREFIX);

        Element root = ec.create("result");
        result.appendChild(root);

        boolean success = backend.deleteUser(userId);

        root.setTextContent(success ? OPERATION_SUCCESSFUL: OPERATION_FAILURE);

        return result;
    }


    // Collection API

    public Document listCollections(String userId, CallMeta callMeta)
        throws ArtifactDatabaseException
    {
        ArtifactCollectionFactory acf = getArtifactCollectionFactory();
        UserFactory               uf  = getUserFactory();

        if (acf == null || uf == null) {
            throw new ArtifactDatabaseException(NO_SUCH_FACTORY);
        }

        logger.debug("Fetch the list of collection for user: " + userId);

        ArtifactCollection [] ac = backend.listCollections(
            userId,
            null, // XXX: fetch from REST
            acf, uf,
            context);

        Document result = XMLUtils.newDocument();

        XMLUtils.ElementCreator ec = new XMLUtils.ElementCreator(
            result,
            ArtifactNamespaceContext.NAMESPACE_URI,
            ArtifactNamespaceContext.NAMESPACE_PREFIX);

        Element root = ec.create("artifact-collections");
        result.appendChild(root);

        if (ac == null || ac.length == 0) {
            logger.debug("No collections for the user existing.");

            return result;
        }

        logger.debug("Found " + ac.length + " collections of the user.");

        for (ArtifactCollection c: ac) {
            Element collection = ec.create("artifact-collection");
            ec.addAttr(collection, "name", c.getName(), true);
            ec.addAttr(collection, "uuid", c.identifier(), true);

            Date creationTime = c.getCreationTime();
            String creation   = creationTime != null
                ? Long.toString(creationTime.getTime())
                : "";

            ec.addAttr(collection, "creation", creation,  true);

            root.appendChild(collection);
        }

        return result;
    }

    public Document createCollection(String ownerId, Document data,
        CallMeta callMeta)
        throws ArtifactDatabaseException
    {
        ArtifactCollectionFactory acf = getArtifactCollectionFactory();

        if (acf == null) {
            throw new ArtifactDatabaseException(NO_SUCH_FACTORY);
        }

        String name = XMLUtils.xpathString(
            data, XPATH_COLLECTION_NAME, ArtifactNamespaceContext.INSTANCE);

        logger.debug("Create new collection with name: " + name);

        Document attr = null;

        Node attrNode = (Node) XMLUtils.xpath(
            data,
            XPATH_COLLECTION_ATTRIBUTE,
            XPathConstants.NODE,
            ArtifactNamespaceContext.INSTANCE);

        if (attrNode != null) {
            attr = XMLUtils.newDocument();
            attr.appendChild(attr.importNode(attrNode, true));
        }

        ArtifactCollection ac = backend.createCollection(
            ownerId, name, acf, attr, context);

        if (ac == null) {
            throw new ArtifactDatabaseException(COLLECTION_CREATION_FAILED);
        }

        Document result = XMLUtils.newDocument();

        XMLUtils.ElementCreator ec = new XMLUtils.ElementCreator(
            result,
            ArtifactNamespaceContext.NAMESPACE_URI,
            ArtifactNamespaceContext.NAMESPACE_PREFIX);

        Element root = ec.create("result");
        result.appendChild(root);

        Element acElement = ec.create("artifact-collection");
        ec.addAttr(acElement, "uuid", ac.identifier(), true);

        root.appendChild(acElement);

        return result;
    }

    public Document deleteCollection(String collectionId, CallMeta callMeta)
        throws ArtifactDatabaseException
    {
        logger.debug("Delete collection: " + collectionId);

        Document result = XMLUtils.newDocument();

        XMLUtils.ElementCreator ec = new XMLUtils.ElementCreator(
            result,
            ArtifactNamespaceContext.NAMESPACE_URI,
            ArtifactNamespaceContext.NAMESPACE_PREFIX);

        Element root = ec.create("result");
        result.appendChild(root);

        boolean success = backend.deleteCollection(collectionId);

        root.setTextContent(success ? OPERATION_SUCCESSFUL: OPERATION_FAILURE);

        return result;
    }

    public Document describeCollection(String collectionId, CallMeta callMeta)
        throws ArtifactDatabaseException
    {
        logger.debug("Describe collection: " + collectionId);
        ArtifactCollectionFactory acf = getArtifactCollectionFactory();

        if (acf == null) {
            throw new ArtifactDatabaseException(NO_SUCH_FACTORY);
        }

        UserFactory uf = getUserFactory();
        if (uf == null) {
            throw new ArtifactDatabaseException(NO_SUCH_FACTORY);
        }

        ArtifactCollection c = backend.getCollection(
            collectionId, acf, uf, context);

        if (c == null) {
            logger.warn("No collection found with identifier: " + collectionId);
            throw new ArtifactDatabaseException(NO_SUCH_COLLECTION);
        }

        CollectionCallContext cc = new CollectionCallContext(
            ArtifactDatabaseImpl.this,
            CallContext.NOTHING,
            callMeta,
            c);

        try {
            return c.describe(cc);
        }
        finally {
            cc.postCall();
        }
    }


    public Document getCollectionAttribute(String collectionId, CallMeta meta)
    throws ArtifactDatabaseException
    {
        logger.debug("Fetch collection attribute for: " + collectionId);

        return backend.getCollectionAttribute(collectionId);
    }


    public Document setCollectionAttribute(
        String   collectionId,
        CallMeta meta,
        Document attribute)
    throws ArtifactDatabaseException
    {
        logger.debug("Set the attribute for the collection: " + collectionId);

        Document attributes = null;

        Node attr = (Node) XMLUtils.xpath(
            attribute,
            XPATH_COLLECTION_ATTRIBUTE,
            XPathConstants.NODE,
            ArtifactNamespaceContext.INSTANCE);

        if (attr != null) {
            attributes = XMLUtils.newDocument();
            attributes.appendChild(attributes.importNode(attr, true));
        }

        Document result = XMLUtils.newDocument();

        XMLUtils.ElementCreator ec = new XMLUtils.ElementCreator(
            result,
            ArtifactNamespaceContext.NAMESPACE_URI,
            ArtifactNamespaceContext.NAMESPACE_PREFIX);

        Element root = ec.create("result");
        result.appendChild(root);

        boolean success = backend.setCollectionAttribute(
            collectionId, attributes);

        root.setTextContent(success ? OPERATION_SUCCESSFUL: OPERATION_FAILURE);

        return result;
    }

    public Document getCollectionItemAttribute(String collectionId, String artifactId,
        CallMeta callMeta) throws ArtifactDatabaseException
    {
        logger.debug("Fetch the attribute for the artifact: " + artifactId);

        return backend.getCollectionItemAttribute(collectionId, artifactId);
    }

    public Document setCollectionItemAttribute(String collectionId, String artifactId,
        Document source, CallMeta callMeta)
        throws ArtifactDatabaseException
    {
        logger.debug("Set the attribute for the artifact: " + artifactId);

        Document attribute = null;

        Node attr = (Node) XMLUtils.xpath(
            source,
            XPATH_COLLECTION_ITEM_ATTRIBUTE,
            XPathConstants.NODE,
            ArtifactNamespaceContext.INSTANCE);

        if (attr != null) {
            attribute = XMLUtils.newDocument();
            attribute.appendChild(attribute.importNode(attr, true));
        }

        Document result = XMLUtils.newDocument();

        XMLUtils.ElementCreator ec = new XMLUtils.ElementCreator(
            result,
            ArtifactNamespaceContext.NAMESPACE_URI,
            ArtifactNamespaceContext.NAMESPACE_PREFIX);

        Element root = ec.create("result");
        result.appendChild(root);

        boolean success = backend.setCollectionItemAttribute(
            collectionId, artifactId, attribute);

        root.setTextContent(success ? OPERATION_SUCCESSFUL: OPERATION_FAILURE);

        return result;
    }

    public Document addCollectionArtifact(
        String   collectionId,
        String   artifactId,
        Document input,
        CallMeta callMeta)
    throws ArtifactDatabaseException
    {
        logger.debug(
            "Add artifact '" + artifactId + "' collection '" +collectionId+"'");

        Document attr = XMLUtils.newDocument();

        Node attrNode = (Node) XMLUtils.xpath(
            input,
            XPATH_COLLECTION_ITEM_ATTRIBUTE,
            XPathConstants.NODE,
            ArtifactNamespaceContext.INSTANCE);

        if (attrNode != null) {
            attr.appendChild(attr.importNode(attrNode, true));
        }

        boolean success = backend.addCollectionArtifact(
            collectionId,
            artifactId,
            attr);

        if (!success) {
            Document result = XMLUtils.newDocument();

            XMLUtils.ElementCreator ec = new XMLUtils.ElementCreator(
                result,
                ArtifactNamespaceContext.NAMESPACE_URI,
                ArtifactNamespaceContext.NAMESPACE_PREFIX);

            Element root = ec.create("result");
            result.appendChild(root);

            root.setTextContent(OPERATION_FAILURE);

            return result;
        }

        return describeCollection(collectionId, callMeta);
    }

    public Document removeCollectionArtifact(String collectionId, String artifactId,
        CallMeta callMeta) throws ArtifactDatabaseException
    {
        logger.debug(
            "Remove artifact '" + artifactId + "' from collection '" +
            collectionId + "'");

        Document attr = XMLUtils.newDocument();

        boolean success = backend.removeCollectionArtifact(
            collectionId,
            artifactId);

        Document result = XMLUtils.newDocument();

        XMLUtils.ElementCreator ec = new XMLUtils.ElementCreator(
            result,
            ArtifactNamespaceContext.NAMESPACE_URI,
            ArtifactNamespaceContext.NAMESPACE_PREFIX);

        Element root = ec.create("result");
        result.appendChild(root);

        root.setTextContent(success ? OPERATION_SUCCESSFUL: OPERATION_FAILURE);

        return result;
    }

    public Document listCollectionArtifacts(String collectionId,
        CallMeta callMeta) throws ArtifactDatabaseException
    {
        CollectionItem[] items = backend.listCollectionArtifacts(collectionId);

        Document result = XMLUtils.newDocument();

        XMLUtils.ElementCreator ec = new XMLUtils.ElementCreator(
            result,
            ArtifactNamespaceContext.NAMESPACE_URI,
            ArtifactNamespaceContext.NAMESPACE_PREFIX);

        Element root = ec.create("result");
        Element ac   = ec.create("artifact-collection");
        ec.addAttr(ac, "uuid", collectionId, true);

        for (CollectionItem item: items) {
            Element i    = ec.create("collection-item");
            Element attr = ec.create("attribute");
            ec.addAttr(i, "uuid", item.getArtifactIdentifier(), true);

            Document attribute = item.getAttribute();
            if (attribute != null) {
                Node firstChild = attribute.getFirstChild();
                attr.appendChild(result.importNode(firstChild, true));
            }
            else {
                logger.debug("No attributes for the collection item!");
            }

            i.appendChild(attr);
            ac.appendChild(i);
        }

        root.appendChild(ac);
        result.appendChild(root);

        return result;
    }

    public DeferredOutput outCollection(String collectionId,
        Document format, CallMeta callMeta)
        throws ArtifactDatabaseException
    {
        ArtifactCollectionFactory acf = getArtifactCollectionFactory();

        if (acf == null) {
            throw new ArtifactDatabaseException(NO_SUCH_FACTORY);
        }

        UserFactory uf = getUserFactory();
        if (uf == null) {
            throw new ArtifactDatabaseException(NO_SUCH_FACTORY);
        }

        ArtifactCollection c = backend.getCollection(
            collectionId, acf, uf, context);

        if (c == null) {
            logger.warn("No collection found with identifier: " + collectionId);
            throw new ArtifactDatabaseException(NO_SUCH_COLLECTION);
        }

        CollectionCallContext cc = new CollectionCallContext(
            ArtifactDatabaseImpl.this,
            CallContext.NOTHING,
            callMeta,
            c);

        try {
            return new DeferredCollectionOutputImpl(c, format, callMeta);
        }
        finally {
            cc.postCall();
        }
    }

    protected void initCallContext(CallContext cc) {
        logger.debug("initCallContext");
        if (callContextListener != null) {
            callContextListener.init(cc);
        }
    }

    protected void closeCallContext(CallContext cc) {
        logger.debug("closeCallContext");
        if (callContextListener != null) {
            callContextListener.close(cc);
        }
    }
}
// vim:set ts=4 sw=4 si et sta sts=4 fenc=utf8 :

http://dive4elements.wald.intevation.org