view flys-artifacts/src/main/java/de/intevation/flys/artifacts/FLYSArtifact.java @ 4496:d8992459b408

Add method to return the facets of an artifact This methos should be used to get the facets of an artifact instead of accessing the facets member variable directly.
author Björn Ricks <bjoern.ricks@intevation.de>
date Wed, 14 Nov 2012 11:11:04 +0100
parents c095eb67c975
children a2735a4bf75e
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. */
    protected 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 = facets.get(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