Mercurial > dive4elements > river
view flys-artifacts/src/main/java/de/intevation/flys/artifacts/FLYSArtifact.java @ 1767:b2730e31ff5b
Store Facets without association to state id.
flys-artifacts/trunk@3088 c6561f87-3c4e-4783-a992-168aeb5c3f6f
author | Felix Wolfsteller <felix.wolfsteller@intevation.de> |
---|---|
date | Wed, 26 Oct 2011 12:45:34 +0000 |
parents | 5d8b3880a553 |
children | c62ff9e72cea |
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 produced by this artifact. */ protected List<Facet> facets; /** * Used to generates "view" on the facets (hides facets not matching the * filter in output of collection); out -> 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 ArrayList<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; } /** * Return a copy of the facet mapping. * @return Copy of facets list. */ protected List<Facet> cloneFacets() { List copy = new ArrayList<Facet>(facets.size()); for (Facet facet: facets) { copy.add(facet.deepCopy()); } return copy; } /** * (called from setup). */ 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! } /** * Builds filter facets from document. * @see filterFacets */ 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. */ 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 = 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); } 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); } public String getDataAsString(String name) { StateData data = getData(name); return data != null ? (String) data.getValue() : null; } /** * 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)); } public Collection<StateData> getAllData() { return data.values(); } public Facet getNativeFacet(Facet facet) { String name = facet.getName(); int index = facet.getIndex(); for (Facet f: facets) { 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; DefaultState current = (DefaultState) getCurrentState(context); 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, current.transform(this, context, name, value)); } else if (name.length() > 0 && value.length() == 0) { if (removeData(name) != null) { logger.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) { 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; } /** * 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()) { logger.debug("No filter for Outputs."); 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; } /** * 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) { 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) { List<Output> list = state.getOutputs(); if (list == null || list.size() == 0) { logger.debug("-> No output modes for this state."); return new ArrayList<Output>(); } if (facets == null || facets.size() == 0) { logger.debug("No facets found."); return new ArrayList<Output>(); } return generateOutputs(list, facets); } /** * 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>(); 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); } /** * 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) { 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.removeAll(fs); } else { facets.addAll(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); } } /** * Destroy the states. */ @Override public void endOfLife(Object context) { logger.info("FLYSArtifact.endOfLife: " + identifier()); List<String> ids = getPreviousStateIds(); ids.add(getCurrentStateId()); destroyStates(ids, context); } /** * Determines Facets initial disposition regarding activity (think of * selection in Client ThemeList GUI). This will be checked one time * when the facet enters a collections describe document. * * @param facetName name of the facet. * @param index index of the facet. * * @return 1 if wished to be initally active, 0 if not. FLYSArtifact * defaults to "1". */ public int getInitialFacetActivity(String facetName, int index) { return 1; } } // vim:set ts=4 sw=4 si et sta sts=4 fenc=utf8 :