view artifact-database/src/main/java/de/intevation/artifactdatabase/ArtifactDatabaseImpl.java @ 114:19b86e27d0c3

New XPath constants and methods that retrieve important nodes of the DESCRIBE document. artifacts/trunk@1328 c6561f87-3c4e-4783-a992-168aeb5c3f6f
author Ingo Weinzierl <ingo.weinzierl@intevation.de>
date Fri, 18 Feb 2011 14:21:32 +0000
parents 933bbc9fc11f
children 4d725248f8d1
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.artifactdatabase.Backend.PersistentArtifact;

import de.intevation.artifacts.Artifact;
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.Service;
import de.intevation.artifacts.ServiceFactory;

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

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

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;

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;

/**
 * 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, Id.Filter, Backend.FactoryLookup
{
    private static Logger logger =
        Logger.getLogger(ArtifactDatabaseImpl.class);

    /**
     * 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 one tries to remove a requested artifact
     * from the list of artifacts running in background which is
     * not in this list.
     */
    public static final String NOT_IN_BACKGROUND =
        "Not in background";

    /**
     * Error message issued if an artifact wants to translate itself
     * into a none valid persistent state.
     */
    public static final String INVALID_CALL_STATE =
        "Invalid after call state";

    /**
     * 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";

    /**
     * Inner class that implements the call context handed
     * to the methods calls describe(), feed(), etc. of the artifact.
     */
    public class CallContextImpl
    implements   CallContext
    {
        /**
         * The persistence wrapper around the living artifact
         */
        protected PersistentArtifact artifact;
        /**
         * The action to be performed after the artifact calls
         * desribe(), feed(), etc. return.
         */
        protected int                action;
        /**
         * The meta information of the concrete call
         * (preferred languages et. al.)
         */
        protected CallMeta           callMeta;
        /**
         * Map to act like a clipboard when nesting calls
         * like a proxy artifact.
         */
        protected HashMap            customValues;

        /**
         * Constructor to create a call context with a given
         * persistent artifact, a default action and meta informations.
         * @param artifact The persistent wrapper around a living artifact.
         * @param action   The action to be performed after the concrete
         * artifact call has returned.
         * @param callMeta The meta information for this call context.
         */
        public CallContextImpl(
            PersistentArtifact artifact,
            int                action,
            CallMeta           callMeta
        ) {
            this.artifact = artifact;
            this.action   = action;
            this.callMeta = callMeta;
        }

        public void afterCall(int action) {
            this.action = action;
            if (action == BACKGROUND) {
                addIdToBackground(artifact.getId());
            }
        }

        public void afterBackground(int action) {
            if (this.action != BACKGROUND) {
                throw new IllegalStateException(NOT_IN_BACKGROUND);
            }
            fromBackground(artifact, action);
        }

        public Object globalContext() {
            return context;
        }

        public ArtifactDatabase getDatabase() {
            return ArtifactDatabaseImpl.this;
        }

        public CallMeta getMeta() {
            return callMeta;
        }

        public Long getTimeToLive() {
            return artifact.getTTL();
        }

        /**
         * Dispatches and executes the persistence action after
         * the return of the concrete artifact call.
         */
        public void postCall() {
            switch (action) {
                case NOTHING:
                    break;
                case TOUCH:
                    artifact.touch();
                    break;
                case STORE:
                    artifact.store();
                    break;
                case BACKGROUND:
                    logger.warn(
                        "BACKGROUND processing is not fully implemented, yet!");
                    artifact.store();
                    break;
                default:
                    logger.error(INVALID_CALL_STATE + ": " + action);
                    throw new IllegalStateException(INVALID_CALL_STATE);
            }
        }

        public Object getContextValue(Object key) {
            return customValues != null
                ? customValues.get(key)
                : null;
        }

        public Object putContextValue(Object key, Object value) {
            if (customValues == null) {
                customValues = new HashMap();
            }
            return customValues.put(key, value);
        }
    } // class CallContextImpl

    /**
     * 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 {

            CallContextImpl cc = new CallContextImpl(
                artifact, CallContext.TOUCH, callMeta);

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

    /**
     * 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;

    /**
     * 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     backgroundIds;

    /**
     * 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();

        setupArtifactFactories(bootstrap);
        setupServices(bootstrap);

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

        wireWithBackend(backend);
    }

    /**
     * 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 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(Integer.valueOf(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 List filterIds(List ids) {
        int N = ids.size();
        ArrayList out = new ArrayList(N);
        synchronized (backgroundIds) {
            for (int i = 0; i < N; ++i) {
                Id id = (Id)ids.get(i);
                // only delete artifact if its not in background.
                if (!backgroundIds.contains(Integer.valueOf(id.getId()))) {
                    out.add(id);
                }
            }
        }
        return out;
    }

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

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

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

    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);
        }

        CallContextImpl cc = new CallContextImpl(
            persistentArtifact, CallContext.NOTHING, callMeta);

        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);
        }

        CallContextImpl cc = new CallContextImpl(
            artifact, CallContext.TOUCH, callMeta);

        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);
        }

        CallContextImpl cc = new CallContextImpl(
            artifact, CallContext.STORE, callMeta);

        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);
        }

        CallContextImpl cc = new CallContextImpl(
            artifact, CallContext.STORE, callMeta);

        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");
        root.appendChild(type);

        Element data = ec.create("data");
        ec.addAttr(data, "checksum", checksum);
        ec.addAttr(data, "factory",  factoryName);
        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);
        }

        CallContextImpl cc = new CallContextImpl(
            persistentArtifact, CallContext.NOTHING, callMeta);

        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);
    }
}
// vim:set ts=4 sw=4 si et sta sts=4 fenc=utf8 :

http://dive4elements.wald.intevation.org