view flys-artifacts/src/main/java/de/intevation/flys/artifacts/FLYSArtifact.java @ 5653:5231e6b849ce

issue1077: Handle overlapping measurement station ranges, refactored Generator, Allow gaps in graph (not sorting, living with NaNs).
author Felix Wolfsteller <felix.wolfsteller@intevation.de>
date Thu, 11 Apr 2013 10:52:56 +0200
parents a2735a4bf75e
children
line wrap: on
line source
package de.intevation.flys.artifacts;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

import javax.xml.xpath.XPathConstants;

import net.sf.ehcache.Cache;

import org.apache.log4j.Logger;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import de.intevation.artifactdatabase.ArtifactDatabaseImpl;
import de.intevation.artifactdatabase.DefaultArtifact;
import de.intevation.artifactdatabase.ProtocolUtils;
import de.intevation.artifactdatabase.data.DefaultStateData;
import de.intevation.artifactdatabase.data.StateData;
import de.intevation.artifactdatabase.state.DefaultFacet;
import de.intevation.artifactdatabase.state.DefaultOutput;
import de.intevation.artifactdatabase.state.Facet;
import de.intevation.artifactdatabase.state.Output;
import de.intevation.artifactdatabase.state.State;
import de.intevation.artifactdatabase.state.StateEngine;
import de.intevation.artifactdatabase.transition.TransitionEngine;
import de.intevation.artifacts.Artifact;
import de.intevation.artifacts.ArtifactDatabase;
import de.intevation.artifacts.ArtifactDatabaseException;
import de.intevation.artifacts.ArtifactFactory;
import de.intevation.artifacts.CallContext;
import de.intevation.artifacts.CallMeta;
import de.intevation.artifacts.Message;
import de.intevation.artifacts.common.ArtifactNamespaceContext;
import de.intevation.artifacts.common.utils.XMLUtils;
import de.intevation.artifacts.common.utils.XMLUtils.ElementCreator;
import de.intevation.flys.artifacts.cache.CacheFactory;
import de.intevation.flys.artifacts.context.FLYSContext;
import de.intevation.flys.artifacts.model.CalculationMessage;
import de.intevation.flys.artifacts.states.DefaultState;
import de.intevation.flys.artifacts.states.DefaultState.ComputeType;
import de.intevation.flys.utils.FLYSUtils;

/**
 * The default FLYS artifact with convenience added.
 * (Subclass to get fully functional artifacts).
 *
 * @author <a href="mailto:ingo.weinzierl@intevation.de">Ingo Weinzierl</a>
 */
public abstract class FLYSArtifact extends DefaultArtifact {

    /** The logger that is used in this artifact. */
    private static Logger log = Logger.getLogger(FLYSArtifact.class);

    public static final String COMPUTING_CACHE = "computed.values";

    /** The XPath that points to the input data elements of the FEED document. */
    public static final String XPATH_FEED_INPUT =
        "/art:action/art:data/art:input";

    /** The XPath that points to the name of the target state of ADVANCE. */
    public static final String XPATH_ADVANCE_TARGET =
        "/art:action/art:target/@art:name";

    public static final String XPATH_MODEL_ARTIFACT =
        "/art:action/art:template/@uuid";

    public static final String XPATH_FILTER =
        "/art:action/art:filter/art:out";

    /** The constant string that shows that an operation was successful. */
    public static final String OPERATION_SUCCESSFUL = "SUCCESS";

    /** The constant string that shows that an operation failed. */
    public static final String OPERATION_FAILED = "FAILURE";

    /** The identifier of the current state. */
    protected String currentStateId;

    /** The identifiers of previous states on a stack. */
    protected List<String> previousStateIds;

    /** The name of the artifact. */
    protected String name;

    /** The data that have been inserted into this artifact. */
    private Map<String, StateData> data;

    /** Mapping of state names to created facets. */
    private Map<String, List<Facet>> facets;

    /**
     * Used to generates "view" on the facets (hides facets not matching the
     * filter in output of collection);  out -&gt; facets.
     */
    protected Map<String, List<Facet>> filterFacets;


    /**
     * The default constructor that creates an empty FLYSArtifact.
     */
    public FLYSArtifact() {
        data             = new TreeMap<String, StateData>();
        previousStateIds = new ArrayList<String>();
        facets           = new HashMap<String, List<Facet>>();
    }


    /**
     * This method appends the static data - that has already been inserted by
     * the user - to the static node of the DESCRIBE document.
     *
     * @param doc The document.
     * @param ui The root node.
     * @param context The CallContext.
     * @param uuid The identifier of the artifact.
     */
    protected void appendStaticUI(
        Document    doc,
        Node        ui,
        CallContext context,
        String uuid)
    {
        List<String> stateIds = getPreviousStateIds();

        FLYSContext flysContext = FLYSUtils.getFlysContext(context);
        StateEngine engine      = (StateEngine) flysContext.get(
            FLYSContext.STATE_ENGINE_KEY);

        boolean debug = log.isDebugEnabled();

        for (String stateId: stateIds) {
            if (debug) {
                log.debug("Append static data for state: " + stateId);
            }
            DefaultState state = (DefaultState) engine.getState(stateId);

            ui.appendChild(state.describeStatic(this, doc, ui, context, uuid));
        }
    }


    /**
     * Returns the name of the concrete artifact.
     *
     * @return the name of the concrete artifact.
     */
    public String getName() {
        return name;
    }


    /**
     * Initialize the artifact and insert new data if <code>data</code> contains
     * information necessary for this artifact.
     *
     * @param identifier The UUID.
     * @param factory The factory that is used to create this artifact.
     * @param context The CallContext.
     * @param data Some optional data.
     */
    @Override
    public void setup(
        String          identifier,
        ArtifactFactory factory,
        Object          context,
        CallMeta        callMeta,
        Document        data)
    {
        boolean debug = log.isDebugEnabled();

        if (debug) {
            log.debug("Setup this artifact with the uuid: " + identifier);
        }

        super.setup(identifier, factory, context, callMeta, data);

        FLYSContext flysContext = FLYSUtils.getFlysContext(context);

        List<State> states = getStates(context);

        String name = getName();

        if (debug) {
            log.debug("setup(): Set initial state for artifact '" + name + "'");
        }

        if (states == null) {
            log.error("No states found from which an initial "
                + "state could be picked.");
        }
        setCurrentState(states.get(0));

        String model = XMLUtils.xpathString(
            data,
            XPATH_MODEL_ARTIFACT,
            ArtifactNamespaceContext.INSTANCE);

        if (model != null && model.length() > 0) {
            ArtifactDatabase db = (ArtifactDatabase) flysContext.get(
                ArtifactDatabaseImpl.GLOBAL_CONTEXT_KEY);

            try {
                initialize(db.getRawArtifact(model), context, callMeta);
            }
            catch (ArtifactDatabaseException adbe) {
                log.error(adbe, adbe);
            }
        }

        filterFacets = buildFilterFacets(data);
    }


    /** Get copy of previous state ids as Strings in list. */
    protected List<String> clonePreviousStateIds() {
        return new ArrayList<String>(previousStateIds);
    }


    /**
     * Copies data item from other artifact to this artifact.
     *
     * @param other Artifact from which to get data.
     * @param name  Name of data.
     */
    protected void importData(FLYSArtifact other, final String name) {
        if (other == null) {
            log.error("No other art. to import data " + name + " from.");
            return;
        }

        StateData sd = other.getData(name);

        if (sd == null) {
            log.warn("Other artifact has no data " + name + ".");
            return;
        }

        this.addData(name, sd);
    }


    /** Clone the internal map of map of state-name to state-data. */
    protected Map<String, StateData> cloneData() {
        Map<String, StateData> copy = new TreeMap<String, StateData>();

        for (Map.Entry<String, StateData> entry: data.entrySet()) {
            copy.put(entry.getKey(), entry.getValue().deepCopy());
        }

        return copy;
    }


    /**
     * Return a copy of the facet mapping.
     * @return Mapping of state-ids to facets.
     */
    protected Map<String, List<Facet>> cloneFacets() {
        Map<String, List<Facet>> copy = new HashMap<String, List<Facet>>();

        for (Map.Entry<String, List<Facet>> entry: facets.entrySet()) {
            List<Facet> facets      = entry.getValue();
            List<Facet> facetCopies = new ArrayList<Facet>(facets.size());
            for (Facet facet: facets) {
                facetCopies.add(facet.deepCopy());
            }
            copy.put(entry.getKey(), facetCopies);
        }

        return copy;
    }


    /**
     * (called from setup).
     * @param artifact master-artifact (if any, otherwise initialize is not called).
     */
    protected void initialize(
        Artifact artifact,
        Object   context,
        CallMeta callMeta)
    {
        if (!(artifact instanceof FLYSArtifact)) {
            return;
        }

        FLYSArtifact flys = (FLYSArtifact)artifact;

        currentStateId   = flys.currentStateId;
        previousStateIds = flys.clonePreviousStateIds();
        name             = flys.name;
        data             = flys.cloneData();
        facets           = flys.cloneFacets();
        // Do not clone filter facets!

        ArrayList<String> stateIds     = (ArrayList<String>) getPreviousStateIds();
        ArrayList<String> toInitialize = (ArrayList<String>) stateIds.clone();

        toInitialize.add(getCurrentStateId());

        for (String stateId: toInitialize) {
            State state = getState(context, stateId);

            if (state != null) {
                state.initialize(artifact, this, context, callMeta);
            }
        }
    }


    /**
     * Builds filter facets from document.
     * @see filterFacets
     */
    protected Map<String, List<Facet>> buildFilterFacets(Document document) {
        if (log.isDebugEnabled()) {
            log.debug("Building filter factes for artifact " + this.getName());
        }

        NodeList nodes = (NodeList)XMLUtils.xpath(
            document,
            XPATH_FILTER,
            XPathConstants.NODESET,
            ArtifactNamespaceContext.INSTANCE);

        if (nodes == null || nodes.getLength() == 0) {
            return null;
        }

        Map<String, List<Facet>> result = new HashMap<String, List<Facet>>();

        for (int i = 0, N = nodes.getLength(); i < N; ++i) {
            Element element = (Element)nodes.item(i);
            String oName = element.getAttribute("name");
            if (oName == null || oName.isEmpty()) {
                continue;
            }

            List<Facet> facets = new ArrayList<Facet>();

            NodeList facetNodes = element.getElementsByTagNameNS(
                ArtifactNamespaceContext.NAMESPACE_URI,
                "facet");

            for (int j = 0, M = facetNodes.getLength(); j < M; ++j) {
                Element facetElement = (Element)facetNodes.item(j);

                String fName = facetElement.getAttribute("name");

                int index;
                try {
                    index = Integer.parseInt(facetElement.getAttribute("index"));
                }
                catch (NumberFormatException nfe) {
                    log.warn(nfe);
                    index = 0;
                }
                if (log.isDebugEnabled()) {
                    log.debug("Creating filter facet " + fName + " with  index " + index +
                            " for out " + oName);
                }
                facets.add(new DefaultFacet(index, fName, ""));
            }

            if (!facets.isEmpty()) {
                result.put(oName, facets);
            }
        }

        return result;
    }


    /**
     * Insert new data included in <code>input</code> into the current state.
     *
     * @param target XML document that contains new data.
     * @param context The CallContext.
     *
     * @return a document that contains a SUCCESS or FAILURE message.
     */
    @Override
    public Document feed(Document target, CallContext context) {
        log.debug("FLYSArtifact.feed()");

        Document doc = XMLUtils.newDocument();

        XMLUtils.ElementCreator creator = new XMLUtils.ElementCreator(
            doc,
            ArtifactNamespaceContext.NAMESPACE_URI,
            ArtifactNamespaceContext.NAMESPACE_PREFIX);

        Element result = creator.create("result");
        doc.appendChild(result);

        try {
            saveData(target, context);

            compute(context, ComputeType.FEED, true);

            return describe(target, context);
        }
        catch (IllegalArgumentException iae) {
            // do not store state if validation fails.
            context.afterCall(CallContext.NOTHING);
            creator.addAttr(result, "type", OPERATION_FAILED, true);

            result.setTextContent(iae.getMessage());
        }

        return doc;
    }


    /**
     * This method returns a description of this artifact.
     *
     * @param data Some data.
     * @param context The CallContext.
     *
     * @return the description of this artifact.
     */
    @Override
    public Document describe(Document data, CallContext context) {

        if (log.isDebugEnabled()) {
            log.debug(
                "Describe: the current state is: " + getCurrentStateId());
            dumpArtifact();
        }

        FLYSContext flysContext = FLYSUtils.getFlysContext(context);

        StateEngine stateEngine = (StateEngine) flysContext.get(
            FLYSContext.STATE_ENGINE_KEY);

        TransitionEngine transitionEngine = (TransitionEngine) flysContext.get(
            FLYSContext.TRANSITION_ENGINE_KEY);

        List<State> reachable = transitionEngine.getReachableStates(
            this, getCurrentState(context), stateEngine);

        Document description            = XMLUtils.newDocument();
        XMLUtils.ElementCreator creator = new XMLUtils.ElementCreator(
            description,
            ArtifactNamespaceContext.NAMESPACE_URI,
            ArtifactNamespaceContext.NAMESPACE_PREFIX);

        Element root = ProtocolUtils.createRootNode(creator);
        description.appendChild(root);

        State current = getCurrentState(context);

        ProtocolUtils.appendDescribeHeader(creator, root, identifier(), hash());
        ProtocolUtils.appendState(creator, root, current);
        ProtocolUtils.appendReachableStates(creator, root, reachable);

        appendBackgroundActivity(creator, root, context);

        Element ui = ProtocolUtils.createArtNode(
            creator, "ui", null, null);

        Element staticUI  = ProtocolUtils.createArtNode(
            creator, "static", null, null);

        Element outs = ProtocolUtils.createArtNode(
            creator, "outputmodes", null, null);
        appendOutputModes(description, outs, context, identifier());

        appendStaticUI(description, staticUI, context, identifier());

        Element name = ProtocolUtils.createArtNode(
            creator, "name",
            new String[] { "value" },
            new String[] { getName() });

        Element dynamic = current.describe(
            this,
            description,
            root,
            context,
            identifier());

        if (dynamic != null) {
            ui.appendChild(dynamic);
        }

        ui.appendChild(staticUI);

        root.appendChild(name);
        root.appendChild(ui);
        root.appendChild(outs);

        return description;
    }

    /** Override me! */

    protected void appendBackgroundActivity(
        ElementCreator cr,
        Element        root,
        CallContext    context
    ) {
        LinkedList<Message> messages = context.getBackgroundMessages();

        if (messages == null) {
            return;
        }

        Element inBackground = cr.create("background-processing");
        root.appendChild(inBackground);

        cr.addAttr(
            inBackground,
            "value",
            String.valueOf(context.isInBackground()),
            true);

        CalculationMessage message = (CalculationMessage) messages.getLast();
        cr.addAttr(
            inBackground,
            "steps",
            String.valueOf(message.getSteps()),
            true);

        cr.addAttr(
            inBackground,
            "currentStep",
            String.valueOf(message.getCurrentStep()),
            true);

        inBackground.setTextContent(message.getMessage());
    }

    /**
     * Append output mode nodes to a document.
     */
    protected void appendOutputModes(
        Document    doc,
        Element     outs,
        CallContext context,
        String      uuid)
    {
        List<Output> generated = getOutputs(context);

        if (log.isDebugEnabled()) {
            log.debug("This Artifact has " + generated.size() + " Outputs.");
        }

        ProtocolUtils.appendOutputModes(doc, outs, generated);
    }


    /**
     * This method handles request for changing the current state of an
     * artifact. It is possible to step forward or backward.
     *
     * @param target The incoming ADVANCE document.
     * @param context The CallContext.
     *
     * @return a document that contains a SUCCESS or FAILURE message.
     */
    @Override
    public Document advance(Document target, CallContext context) {

        boolean debug = log.isDebugEnabled();

        Document doc = XMLUtils.newDocument();

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

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

        String currentStateId = getCurrentStateId();
        String targetState    = XMLUtils.xpathString(
            target, XPATH_ADVANCE_TARGET, ArtifactNamespaceContext.INSTANCE);

        if (debug) {
            log.debug("FLYSArtifact.advance() to '" + targetState + "'");
        }

        if (!currentStateId.equals(targetState)
            && isStateReachable(targetState, context))
        {
            if (debug) {
                log.debug("Advance: Step forward");
            }

            List<String> prev = getPreviousStateIds();
            prev.add(currentStateId);

            setCurrentStateId(targetState);

            if (debug) {
                log.debug("Compute data for state: " + targetState);
            }
            compute(context, ComputeType.ADVANCE, true);

            return describe(target, context);
        }
        else if (isPreviousState(targetState, context)) {
            if (debug) {
                log.debug("Advance: Step back to");
            }

            List<String> prevs   = getPreviousStateIds();
            int targetIdx        = prevs.indexOf(targetState);
            int start            = prevs.size() - 1;

            destroyStates(prevs, context);

            for (int i = start; i >= targetIdx; i--) {
                String prev = prevs.get(i);
                if (debug) {
                    log.debug("Remove state id '" + prev + "'");
                }

                prevs.remove(prev);
                facets.remove(prev);
            }

            destroyState(getCurrentStateId(), context);
            setCurrentStateId(targetState);

            return describe(target, context);
        }

        log.warn("Advance: Cannot advance to '" + targetState + "'");
        ec.addAttr(result, "type", OPERATION_FAILED, true);

        doc.appendChild(result);

        return doc;
    }


    /**
     * Returns the identifier of the current state.
     *
     * @return the identifier of the current state.
     */
    public String getCurrentStateId() {
        return currentStateId;
    }


    /**
     * Sets the identifier of the current state.
     *
     * @param id the identifier of a state.
     */
    protected void setCurrentStateId(String id) {
        currentStateId = id;
    }


    /**
     * Set the current state of this artifact. <b>NOTE</b>We don't store the
     * State object itself - which is not necessary - but its identifier. So
     * this method will just call the setCurrentStateId() method with the
     * identifier of <i>state</i>.
     *
     * @param state The new current state.
     */
    protected void setCurrentState(State state) {
        setCurrentStateId(state.getID());
    }


    /**
     * Returns the current state of the artifact.
     *
     * @return the current State of the artifact.
     */
    public State getCurrentState(Object context) {
        return getState(context, getCurrentStateId());
    }


    /**
     * Get list of existant states for this Artifact.
     * @param context Contex to get StateEngine from.
     * @return list of states.
     */
    protected List<State> getStates(Object context) {
        FLYSContext flysContext = FLYSUtils.getFlysContext(context);
        StateEngine engine      = (StateEngine) flysContext.get(
            FLYSContext.STATE_ENGINE_KEY);
        return engine.getStates(getName());
    }


    /**
     * Get state with given ID.
     * @param context Context to get StateEngine from.
     * @param stateID ID of state to get.
     * @return state with given ID.
     */
    protected State getState(Object context, String stateID) {
        FLYSContext flysContext = FLYSUtils.getFlysContext(context);
        StateEngine engine      = (StateEngine) flysContext.get(
            FLYSContext.STATE_ENGINE_KEY);
        return engine.getState(stateID);
    }


    /**
     * Returns the vector of previous state identifiers.
     *
     * @return the vector of previous state identifiers.
     */
    protected List<String> getPreviousStateIds() {
        return previousStateIds;
    }


    /**
     * Get all previous and the current state id.
     * @return #getPreviousStateIds() + #getCurrentStateId()
     */
    public List<String> getStateHistoryIds() {
        ArrayList<String> prevIds = (ArrayList) getPreviousStateIds();
        ArrayList<String> allIds  = (ArrayList) prevIds.clone();

        allIds.add(getCurrentStateId());
        return allIds;
    }


    /**
     * Adds a new StateData item to the data pool of this artifact.
     *
     * @param name the name of the data object.
     * @param data the data object itself.
     */
    protected void addData(String name, StateData data) {
        this.data.put(name, data);
    }


    /** Remove and return statedata associated to name. */
    protected StateData removeData(String name) {
        return this.data.remove(name);
    }


    /**
     * This method returns a specific StateData object that is stored in the
     * data pool of this artifact.
     *
     * @param name The name of the data object.
     *
     * @return the StateData object if existing, otherwise null.
     */
    public StateData getData(String name) {
        return data.get(name);
    }


    /**
     * A derived Artifact class can use this method to set the data
     */
    protected void setData(Map<String, StateData> data) {
        this.data = data;
    }


    /** Return named data item, null if not found. */
    public String getDataAsString(String name) {
        StateData data = getData(name);
        return data != null ? (String) data.getValue() : null;
    }


    /**
     * This method returns the value of a StateData object stored in the data
     * pool of this Artifact as Integer.
     *
     * @param name The name of the StateData object.
     *
     * @return an Integer representing the value of the data object or null if
     * no object was found for <i>name</i>.
     *
     * @throws NumberFormatException if the value of the data object could not
     * be transformed into an Integer.
     */
    public Integer getDataAsInteger(String name)
    throws NumberFormatException
    {
        String value = getDataAsString(name);

        if (value != null && value.length() > 0) {
            return Integer.parseInt(value);
        }

        return null;
    }


    /**
     * This method returns the value of a StateData object stored in the data
     * pool of this Artifact as Double.
     *
     * @param name The name of the StateData object.
     *
     * @return an Double representing the value of the data object or null if
     * no object was found for <i>name</i>.
     *
     * @throws NumberFormatException if the value of the data object could not
     * be transformed into a Double.
     */
    public Double getDataAsDouble(String name)
    throws NumberFormatException
    {
        String value = getDataAsString(name);

        if (value != null && value.length() > 0) {
            return Double.parseDouble(value);
        }

        return null;
    }


    /**
     * This method returns the value of a StateData object stored in the data
     * pool of this Artifact as Long.
     *
     * @param name The name of the StateData object.
     *
     * @return a Long representing the value of the data object or null if
     * no object was found for <i>name</i>.
     *
     * @throws NumberFormatException if the value of the data object could not
     * be transformed into a Long.
     */
    public Long getDataAsLong(String name)
    throws NumberFormatException
    {
        String value = getDataAsString(name);

        if (value != null && value.length() > 0) {
            return Long.parseLong(value);
        }

        return null;
    }


    /**
     * This method returns the value of a StateData object stored in the data
     * pool of this Artifact is Boolean using Boolean.valueOf().
     *
     * @param name The name of the StateData object.
     *
     * @return a Boolean representing the value of the data object or null if no
     * such object is existing.
     */
    public Boolean getDataAsBoolean(String name) {
        String value = getDataAsString(name);

        if (value == null || value.length() == 0) {
            return null;
        }

        return Boolean.valueOf(value);
    }


    /**
     * Add StateData containing a given string.
     * @param name Name of the data object.
     * @param value String to store.
     */
    public void addStringData(String name, String value) {
        addData(name, new DefaultStateData(name, null, null, value));
    }

    /**
     * This method returns all stored StateData in this artifact as a Collection
     * @return a Collection of all StateData objects in this artifact
     */
    public Collection<StateData> getAllData() {
        return data.values();
    }


    /** Return all produced facets. */
    public List<Facet> getFacets() {
        List<Facet> all = new ArrayList<Facet>();

        // Iterate over facets of each state.
        for (List<Facet> fs: facets.values()) {
            all.addAll(fs);
        }

        return all;
    }


    /**
     * Get facet as stored internally, with equalling name and index than given
     * facet.
     * @param facet that defines index and name of facet searched.
     * @return facet instance or null if not found.
     */
    public Facet getNativeFacet(Facet facet) {
        String name  = facet.getName();
        int    index = facet.getIndex();

        for (List<Facet> fs: facets.values()) {
            for (Facet f: fs) {
                if (f.getIndex() == index && f.getName().equals(name)) {
                    return f;
                }
            }
        }

        log.warn("Could not find facet: " + name + " at " + index);
        log.warn("Available facets for : " + getName() + " " + identifier() +
                ": " + facets.values());
        return null;
    }


    /**
     * This method stores the data that is contained in the FEED document.
     *
     * @param feed The FEED document.
     * @param xpath The XPath that points to the data nodes.
     */
    public void saveData(Document feed, CallContext context)
    throws IllegalArgumentException
    {
        if (feed == null) {
            throw new IllegalArgumentException("error_feed_no_data");
        }

        NodeList nodes = (NodeList) XMLUtils.xpath(
            feed,
            XPATH_FEED_INPUT,
            XPathConstants.NODESET,
            ArtifactNamespaceContext.INSTANCE);

        if (nodes == null || nodes.getLength() == 0) {
            throw new IllegalArgumentException("error_feed_no_data");
        }

        boolean debug = log.isDebugEnabled();

        int count = nodes.getLength();

        if (debug) {
            log.debug("Try to save " + count + " data items.");
        }

        String uri = ArtifactNamespaceContext.NAMESPACE_URI;

        DefaultState current = (DefaultState) getCurrentState(context);

        FLYSContext flysContext = FLYSUtils.getFlysContext(context);
        StateEngine engine      = (StateEngine) flysContext.get(
            FLYSContext.STATE_ENGINE_KEY);

        for (int i = 0; i < count; i++) {
            Element node = (Element)nodes.item(i);

            String name  = node.getAttributeNS(uri, "name");
            String value = node.getAttributeNS(uri, "value");

            if (name.length() > 0 && value.length() > 0) {
                if (debug) {
                    log.debug("Save data item for '" + name + "' : " + value);
                }

                StateData model = engine.getStateData(getName(), name);

                StateData sd = model != null
                    ? model.deepCopy()
                    : new DefaultStateData(name, null, null, value);

                addData(
                    name, current.transform(this, context, sd, name, value));
            }
            else if (name.length() > 0 && value.length() == 0) {
                if (removeData(name) != null && debug) {
                    log.debug("Removed data '" + name + "' successfully.");
                }
            }
        }

        current.validate(this);
    }


    /**
     * Determines if the state with the identifier <i>stateId</i> is reachable
     * from the current state. The determination itself takes place in the
     * TransitionEngine.
     *
     * @param stateId The identifier of a state.
     * @param context The context object.
     *
     * @return true, if the state specified by <i>stateId</i> is reacahble,
     * otherwise false.
     */
    protected boolean isStateReachable(String stateId, Object context) {

        if (log.isDebugEnabled()) {
            log.debug("Determine if the state '" + stateId + "' is reachable.");
        }

        FLYSContext flysContext = FLYSUtils.getFlysContext(context);

        State currentState  = getCurrentState(context);
        StateEngine sEngine = (StateEngine) flysContext.get(
            FLYSContext.STATE_ENGINE_KEY);

        TransitionEngine tEngine = (TransitionEngine) flysContext.get(
            FLYSContext.TRANSITION_ENGINE_KEY);

        return tEngine.isStateReachable(this, stateId, currentState, sEngine);
    }


    /**
     * Determines if the state with the identifier <i>stateId</i> is a previous
     * state of the current state.
     *
     * @param stateId The target state identifier.
     * @param context The context object.
     */
    protected boolean isPreviousState(String stateId, Object context) {
        if (log.isDebugEnabled()) {
            log.debug("Determine if the state '" + stateId + "' is old.");
        }

        return getPreviousStateIds().contains(stateId);
    }


    /**
     * Computes the hash code of the entered values.
     *
     * @return a hash code.
     */
    @Override
    public String hash() {

        long hash  = 0L;
        int  shift = 3;

        for (Map.Entry<String, StateData> entry: data.entrySet()) {
            String key   = entry.getKey();
            Object value = entry.getValue().getValue();

            hash ^= ((long)key.hashCode() << shift)
                 |  ((long)value.hashCode() << (shift + 3));
            shift += 2;
        }

        return getCurrentStateId() + hash;
    }


    /**
     * Return List of outputs, where combinations of outputname and filtername
     * that match content in filterFacets is left out.
     * @return filtered Outputlist.
     */
    protected List<Output> filterOutputs(List<Output> outs) {
        if (filterFacets == null || filterFacets.isEmpty()) {
            log.debug("No filter for Outputs.");
            return outs;
        }

        boolean debug = log.isDebugEnabled();

        if (debug) {
            log.debug(
                "Filter Facets with " + filterFacets.size() + " filters.");
        }

        List<Output> filtered = new ArrayList<Output>();

        for (Output out: outs) {
            String outName = out.getName();

            if (debug) {
                log.debug("  filter Facets for Output: " + outName);
            }

            List<Facet> fFacets = filterFacets.get(outName);
            if (fFacets != null) {
                if (debug) {
                    log.debug("" + fFacets.size() + " filters for: " + outName);
                    for (Facet tmp: fFacets) {
                        log.debug("   filter = '" + tmp.getName() + "'");
                    }
                }

                List<Facet> resultFacets = new ArrayList<Facet>();

                for (Facet facet: out.getFacets()) {
                    for (Facet fFacet: fFacets) {
                        if (facet.getIndex() == fFacet.getIndex()
                        &&  facet.getName().equals(fFacet.getName())) {
                            resultFacets.add(facet);
                            break;
                        }
                    }
                }

                if (debug) {
                    log.debug(
                        "Facets after filtering = " + resultFacets.size());
                }

                if (!resultFacets.isEmpty()) {
                    DefaultOutput nout = new DefaultOutput(
                        out.getName(),
                        out.getDescription(),
                        out.getMimeType(),
                        resultFacets);
                    filtered.add(nout);
                }
            }
            else if (debug) {
                log.debug("No filter Factes for Output: " + outName);
            }
        }

        if (debug) {
            log.debug("Number of outs after filtering = " + filtered.size());
        }

        return filtered;
    }


    /**
     * Get all outputs that the Artifact can do in this state (which includes
     * all previous states).
     *
     * @return list of outputs
     */
    public List<Output> getOutputs(Object context) {
        if (log.isDebugEnabled()) {
            log.debug("##### Get Outputs for: " + identifier() + " #####");
            dumpArtifact();
        }

        List<String> stateIds  = getPreviousStateIds();
        List<Output> generated = new ArrayList<Output>();

        for (String stateId: stateIds) {
            DefaultState state = (DefaultState) getState(context, stateId);
            generated.addAll(getOutputForState(state));
        }

        generated.addAll(getCurrentOutputs(context));

        return filterOutputs(generated);
    }


    /**
     * Get output(s) for current state.
     * @return list of outputs for current state.
     */
    public List<Output> getCurrentOutputs(Object context) {
        DefaultState cur = (DefaultState) getCurrentState(context);

        try {
            if (cur.validate(this)) {
                return getOutputForState(cur);
            }
        }
        catch (IllegalArgumentException iae) { }

        return new ArrayList<Output>();
    }


    /**
     * Get output(s) for a specific state.
     * @param state State of interest
     * @return list of output(s) for given state.
     */
    protected List<Output> getOutputForState(DefaultState state) {

        if (state == null) {
            log.error("state == null: This should not happen!");
            return new ArrayList<Output>();
        }

        boolean debug = log.isDebugEnabled();

        if (debug) {
            log.debug("Find Outputs for State: " + state.getID());
        }

        List<Output> list = state.getOutputs();
        if (list == null || list.isEmpty()) {
            if (debug) {
                log.debug("-> No output modes for this state.");
            }
            return new ArrayList<Output>();
        }

        String stateId = state.getID();

        List<Facet> fs = getFacets(stateId);

        if (fs == null || fs.isEmpty()) {
            if (debug) {
                log.debug("No facets found.");
            }
            return new ArrayList<Output>();
        }
        if (debug) {
            log.debug("State '" + stateId + "' has facets " + fs);
        }

        List<Output> gen = generateOutputs(list, fs);

        if (debug) {
            log.debug("State '" + stateId + "' has " + gen.size() + " outs");
        }

        return gen;
    }


    /**
     * Generate a list of outputs with facets from fs if type is found in list
     * of output.
     *
     * @param list List of outputs
     * @param fs List of facets
     */
    protected List<Output> generateOutputs(List<Output> list, List<Facet> fs) {
        List<Output> generated = new ArrayList<Output>();
        log.debug("generateOutputs for Artifact " + getName() + " "
                + identifier());

        boolean debug = log.isDebugEnabled();

        for (Output out: list) {
            log.debug("check facets for output: " + out.getName());
            Output o = new DefaultOutput(
                out.getName(),
                out.getDescription(),
                out.getMimeType(),
                out.getType());

            Set<String> outTypes = new HashSet<String>();

            for (Facet f: out.getFacets()) {
                if (outTypes.add(f.getName()) && debug) {
                    log.debug("configured facet " + f);
                }
            }

            boolean facetAdded = false;
            for (Facet f: fs) {
                String type = f.getName();

                if (outTypes.contains(type)) {
                    if (debug) {
                        log.debug("Add facet " + f);
                    }
                    facetAdded = true;
                    o.addFacet(f);
                }
            }

            if (facetAdded) {
                generated.add(o);
            }
        }

        return generated;
    }


    /**
     * Dispatches the computation request to compute(CallContext context, String
     * hash) with the current hash value of the artifact which is provided by
     * hash().
     *
     * @param context The CallContext.
     */
    public Object compute(
        CallContext context,
        ComputeType type,
        boolean     generateFacets
    ) {
        return compute(context, hash(), type, generateFacets);
    }


    /**
     * Dispatches computation requests to the current state which needs to
     * implement a createComputeCallback(String hash, FLYSArtifact artifact)
     * method.
     *
     * @param context The CallContext.
     * @param hash The hash value which is used to fetch computed data from
     * cache.
     *
     * @return the computed data.
     */
    public Object compute(
        CallContext context,
        String      hash,
        ComputeType type,
        boolean     generateFacets
    ) {
        DefaultState current = (DefaultState) getCurrentState(context);
        return compute(context, hash, current, type, generateFacets);
    }


    /**
     * Like compute, but identify State by it id (string).
     */
    public Object compute(
        CallContext context,
        String      hash,
        String      stateID,
        ComputeType type,
        boolean     generateFacets
    ) {
        DefaultState current =
            (stateID == null)
            ? (DefaultState)getCurrentState(context)
            : (DefaultState)getState(context, stateID);

        if (hash == null) {
            hash = hash();
        }

        return compute(context, hash, current, type, generateFacets);
    }


    /**
     * Let current state compute and register facets.
     *
     * @param key key of state
     * @param state state
     * @param type Type of compute
     * @param generateFacets Whether new facets shall be generated.
     */
    public Object compute(
        CallContext   context,
        String        key,
        DefaultState  state,
        ComputeType   type,
        boolean       generateFacets
    ) {
        String stateID = state.getID();

        List<Facet> fs = (generateFacets) ? new ArrayList<Facet>() : null;

        try {
            Cache cache = CacheFactory.getCache(COMPUTING_CACHE);

            Object old = null;

            if (cache != null) {
                net.sf.ehcache.Element element = cache.get(key);
                if (element != null) {
                    log.debug("Got computation result from cache.");
                    old = element.getValue();
                }
            }
            else {
                log.debug("cache not configured.");
            }

            Object res;
            switch (type) {
                case FEED:
                    res = state.computeFeed(this, key, context, fs, old);
                    break;
                case ADVANCE:
                    res = state.computeAdvance(this, key, context, fs, old);
                    break;
                case INIT:
                    res = state.computeInit(this, key, context, context.getMeta(), fs);
                default:
                    res = null;
            }

            if (cache != null && old != res && res != null) {
                log.debug("Store computation result to cache.");
                net.sf.ehcache.Element element =
                    new net.sf.ehcache.Element(key, res);
                cache.put(element);
            }

            return res;
        }
        finally {
            if (generateFacets) {
                if (fs.isEmpty()) {
                    facets.remove(stateID);
                }
                else {
                    addFacets(stateID, fs);
                }
            }
        }
    }

    /**
     * Sets the facets for an ID
     *
     * Normally the id is a state ID.
     *
     * @param id ID to map the facets to
     * @param facets List of facets to be stored
     */
    protected void addFacets(String id, List<Facet> facets) {
        this.facets.put(id, facets);
    }


    /**
     * Method to dump the artifacts state/data.
     */
    protected void dumpArtifact() {
        log.debug("++++++++++++++ DUMP ARTIFACT DATA +++++++++++++++++");
        // Include uuid, type, name
        log.debug(" - Name: " + getName());
        log.debug(" - UUID: " + identifier());
        log.debug(" - Class: " + this.getClass().getName());

        log.debug("------ DUMP DATA ------");
        Collection<StateData> allData = data.values();

        for (StateData d: allData) {
            String name  = d.getName();
            String value = (String) d.getValue();

            log.debug("- " + name + ": " + value);
        }

        log.debug("------ DUMP PREVIOUS STATES ------");
        List<String> stateIds = getPreviousStateIds();

        for (String id: stateIds) {
            log.debug("- State: " + id);
        }

        log.debug("CURRENT STATE: " + getCurrentStateId());

        debugFacets();
        dumpFilterFacets();

        log.debug("++++++++++++++ END ARTIFACT DUMP +++++++++++++++++");
    }


    protected void debugFacets() {
        log.debug("######### FACETS #########");

        for (Map.Entry<String, List<Facet>> entry: facets.entrySet()) {
            String out = entry.getKey();
            List<Facet> fs = entry.getValue();
            for (Facet f: fs) {
                log.debug("  # " + out + " : " + f.getName());
            }
        }

        log.debug("######## FACETS END ########");
    }


    protected void dumpFilterFacets() {
        log.debug("######## FILTER FACETS ########");

        if (filterFacets == null || filterFacets.isEmpty()) {
            log.debug("No Filter Facets defined.");
            return;
        }

        for (Map.Entry<String, List<Facet>> entry: filterFacets.entrySet()) {
            String      out     = entry.getKey();
            List<Facet> filters = entry.getValue();

            log.debug("There are " + filters.size() + " filters for: " +out);

            for (Facet filter: filters) {
                log.debug("  filter: " + filter.getName());
            }
        }

        log.debug("######## FILTER FACETS END ########");
    }


    /** Destroy and clean up state with given id. */
    protected void destroyState(String id, Object context) {
        State s = getState(context, id);
        s.endOfLife(this, context);
    }


    /**
     * Calls endOfLife() for each state in the list <i>ids</i>.
     *
     * @param ids The State IDs that should be destroyed.
     * @param context The FLYSContext.
     */
    protected void destroyStates(List<String> ids, Object context) {
        for (int i = 0, num = ids.size(); i < num; i++) {
            destroyState(ids.get(i), context);
        }
    }


    /**
     * Destroy the states.
     */
    @Override
    public void endOfLife(Object context) {
        if (log.isDebugEnabled()) {
            log.debug("FLYSArtifact.endOfLife: " + identifier());
        }

        ArrayList<String> ids       = (ArrayList<String>) getPreviousStateIds();
        ArrayList<String> toDestroy = (ArrayList<String>) ids.clone();

        toDestroy.add(getCurrentStateId());

        destroyStates(toDestroy, context);
    }

    /**
     * Return the Facets which a state provides
     * @param stateid String that identifies the state
     * @return List of Facets belonging to the state identifier
     */
    protected List<Facet> getFacets(String stateid) {
        return this.facets.get(stateid);
    }
}
// vim:set ts=4 sw=4 si et sta sts=4 fenc=utf8 :

http://dive4elements.wald.intevation.org