view flys-artifacts/src/main/java/de/intevation/flys/artifacts/FLYSArtifact.java @ 1125:65d8b3340397

Cleanups of CrossSection*. flys-artifacts/trunk@2635 c6561f87-3c4e-4783-a992-168aeb5c3f6f
author Felix Wolfsteller <felix.wolfsteller@intevation.de>
date Thu, 01 Sep 2011 13:00:36 +0000
parents ea80e652f033
children 073c36bb94ed
line wrap: on
line source
package de.intevation.flys.artifacts;

import de.intevation.artifactdatabase.ArtifactDatabaseImpl;
import de.intevation.artifactdatabase.DefaultArtifact;

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.common.ArtifactNamespaceContext;

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

import de.intevation.flys.artifacts.cache.CacheFactory;

import de.intevation.flys.artifacts.context.FLYSContext;

import de.intevation.flys.artifacts.states.DefaultState.ComputeType;

import de.intevation.flys.artifacts.states.DefaultState;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
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.NodeList;

/**
 * The defaul FLYS artifact.
 *
 * @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 logger = 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.*/
    protected Map<String, StateData> data;

    /** The list of facets supported by this artifact.*/
    protected Map<String, List<Facet>> facets;

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


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

    /**
     * Returns the FLYSContext from context object.
     *
     * @param context The CallContext or the FLYSContext.
     *
     * @return the FLYSContext.
     */
    protected static FLYSContext getFlysContext(Object context) {
        return context instanceof FLYSContext
            ? (FLYSContext) context
            : (FLYSContext) ((CallContext) context).globalContext();
    }


    /**
     * 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)
    {
        logger.debug("Setup this artifact with the uuid: " + identifier);

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

        FLYSContext flysContext = getFlysContext(context);

        List<State> states = getStates(context);

        String name = getName();
        logger.debug("Set initial state for artifact '" + name + "'");

        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) {
                logger.error(adbe, adbe);
            }
        }

        filterFacets = buildFilterFacets(data);
    }

    protected List<String> clonePreviousStateIds() {
        return new ArrayList<String>(previousStateIds);
    }

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

    protected Map<String, List<Facet>> cloneFacets() {
        Map 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;
    }

    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!
    }

    protected Map<String, List<Facet>> buildFilterFacets(Document document) {

        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.length() == 0) {
                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) {
                    logger.warn(nfe);
                    index = 0;
                }
                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) {
        logger.info("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, XPATH_FEED_INPUT, 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 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.
     */
    public Document advance(Document target, CallContext context) {
        Document doc = XMLUtils.newDocument();

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

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

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

        logger.info("FLYSArtifact.advance() to '" + targetState + "'");

        if (isStateReachable(targetState, context)) {
            logger.info("Advance: Step forward");

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

            setCurrentStateId(targetState);

            logger.debug("Compute data for state: " + targetState);
            compute(context, ComputeType.ADVANCE, true);

            return describe(target, context);
        }
        else if (isPreviousState(targetState, context)) {
            logger.info("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);
                logger.debug("Remove state id '" + prev + "'");

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

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

            return describe(target, context);
        }

        logger.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.
     */
    protected 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 = 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 = 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;
    }


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


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


    public String getDataAsString(String name) {
        StateData data = getData(name);
        return data != null ? (String) data.getValue() : null;
    }

    public Collection<StateData> getAllData() {
        return data.values();
    }


    public Facet getNativeFacet(Facet facet) {
        String name  = facet.getName();
        int    index = facet.getIndex();

        for (Map.Entry<String, List<Facet>> entry: facets.entrySet()) {
            for (Facet f: entry.getValue()) {
                if (f.getIndex() == index && f.getName().equals(name)) {
                    return f;
                }
            }
        }

        logger.warn("Could not find facet: " + name + " at " + index);
        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, String xpath, CallContext context)
    throws IllegalArgumentException
    {
        if (feed == null || xpath == null || xpath.length() == 0) {
            throw new IllegalArgumentException("error_feed_no_data");
        }

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

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

        int count = nodes.getLength();
        logger.debug("Try to save " + count + " data items.");

        String uri = ArtifactNamespaceContext.NAMESPACE_URI;

        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) {
                logger.debug("Save data item for '" + name + "' : " + value);

                addData(name, new DefaultStateData(name, null, null, value));
            }
        }

        DefaultState current = (DefaultState) getCurrentState(context);
        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) {
        logger.debug("Determine if the state '" + stateId + "' is reachable.");

        FLYSContext flysContext = 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) {
        logger.debug("Determine if the state '" + stateId + "' is old.");

        List<String> prevs = getPreviousStateIds();
        if (prevs.contains(stateId)) {
            return true;
        }

        return false;
    }


    /**
     * Computes the hash code of the entered values.
     *
     * @return a hash code.
     */
    @Override
    public String hash() {
        Set<Map.Entry<String, StateData>> entries = data.entrySet();

        long hash  = 0L;
        int  shift = 3;

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

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

        return getCurrentStateId() + hash;
    }

    protected List<Output> filterOutputs(List<Output> outs) {

        if (filterFacets == null || filterFacets.isEmpty()) {
            return outs;
        }

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

        for (Output out: outs) {

            List<Facet> fFacets = filterFacets.get(out.getName());
            if (fFacets != null) {

                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 (!resultFacets.isEmpty()) {
                    DefaultOutput nout = new DefaultOutput(
                        out.getName(),
                        out.getDescription(),
                        out.getMimeType(),
                        resultFacets);
                    filtered.add(nout);
                }
            }
        }

        return filtered;
    }


    public List<Output> getOutputs(Object context) {
        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);
    }


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

    protected List<Output> getOutputForState(DefaultState state) {
        List<Output> list = state.getOutputs();
        if (list == null || list.size() == 0) {
            logger.debug("-> No output modes for this state.");
            return new ArrayList<Output>();
        }

        List<Facet>  fs = facets.get(state.getID());
        if (fs == null || fs.size() == 0) {
            logger.debug("No facets found.");
            return new ArrayList<Output>();
        }

        return generateOutputs(list, fs);
    }


    protected List<Output> generateOutputs(List<Output> list, List<Facet> fs) {
        List<Output> generated = new ArrayList<Output>();

        boolean debug = logger.isDebugEnabled();

        for (Output out: list) {
            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) {
                    logger.debug("configured facet " + f);
                }
            }

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

                if (outTypes.contains(type)) {
                    if (debug) {
                        logger.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);
    }

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

    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) {
                    logger.debug("Got computation result from cache.");
                    old = element.getValue();
                }
            }

            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) {
                logger.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 {
                    facets.put(stateID, fs);
                }
            }
        }
    }


    /**
     * Method to dump the artifacts state/data.
     */
    protected void dumpArtifact() {
        if (logger.isDebugEnabled()) {
            logger.debug("++++++++++++++ DUMP ARTIFACT DATA +++++++++++++++++");

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

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

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

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

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

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

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


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


    @Override
    public void endOfLife(Object context) {
        logger.info("FLYSArtifact.endOfLife: " + identifier());

        List<String> ids = getPreviousStateIds();
        ids.add(getCurrentStateId());

        destroyStates(ids, context);
    }
}
// vim:set ts=4 sw=4 si et sta sts=4 fenc=utf8 :

http://dive4elements.wald.intevation.org