view flys-artifacts/src/main/java/de/intevation/flys/collections/FLYSArtifactCollection.java @ 3870:0c16eace7b6c

Add robustness checks to prevent NPEs flys-artifacts/trunk@5502 c6561f87-3c4e-4783-a992-168aeb5c3f6f
author Christian Lins <christian.lins@intevation.de>
date Tue, 18 Sep 2012 10:18:30 +0000
parents 53aa395a29e0
children 3aec5a42696a
line wrap: on
line source
package de.intevation.flys.collections;

import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.xml.xpath.XPathConstants;

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.Backend;
import de.intevation.artifactdatabase.Backend.PersistentArtifact;
import de.intevation.artifactdatabase.DefaultArtifactCollection;
import de.intevation.artifactdatabase.state.Output;
import de.intevation.artifactdatabase.state.Settings;
import de.intevation.artifactdatabase.state.StateEngine;
import de.intevation.artifacts.Artifact;
import de.intevation.artifacts.ArtifactDatabase;
import de.intevation.artifacts.ArtifactDatabaseException;
import de.intevation.artifacts.ArtifactNamespaceContext;
import de.intevation.artifacts.CallContext;
import de.intevation.artifacts.CallMeta;
import de.intevation.artifacts.common.utils.XMLUtils;
import de.intevation.flys.artifacts.FLYSArtifact;
import de.intevation.flys.artifacts.context.FLYSContext;
import de.intevation.flys.exports.OutGenerator;
import de.intevation.flys.exports.OutputHelper;
import de.intevation.flys.utils.FLYSUtils;

/**
 * @author <a href="mailto:ingo.weinzierl@intevation.de">Ingo Weinzierl</a>
 */
public class FLYSArtifactCollection extends DefaultArtifactCollection {
    /** The logger used in this class. */
    private static Logger log = Logger.getLogger(FLYSArtifactCollection.class);

    /** Constant XPath that points to the outputmodes of an artifact. */
    public static final String XPATH_ARTIFACT_OUTPUTMODES =
        "/art:result/art:outputmodes";

    public static final String XPATH_ARTIFACT_STATE_DATA =
        "/art:result/art:ui/art:static/art:state/art:data";

    public static final String XPATH_COLLECTION_ITEMS =
        "/art:result/art:artifact-collection/art:collection-item";

    public static final String XPATH_OUT_NAME = "/art:action/@art:name";

    public static final String XPATH_OUT_TYPE = "/art:action/@art:type";

    /** Xpath to master artifacts uuid. */
    public static final String XPATH_MASTER_UUID =
        "/art:artifact-collection/art:artifact/@art:uuid";

    public static final String XPATH_LOADED_RECOMMENDATIONS =
        "/art:attribute/art:loaded-recommendations";


    /**
     * Return description Document for this collection.
     */
    @Override
    public Document describe(CallContext context) {
        log.debug("FLYSArtifactCollection.describe: " + identifier);

        CollectionDescriptionHelper helper = new CollectionDescriptionHelper(
            getName(), identifier(), getCreationTime(), getTTL(),
            context);

        ArtifactDatabase db = context.getDatabase();

        Document        oldAttrs = getAttribute();
        AttributeParser parser   = new AttributeParser(oldAttrs);

        try {
            String[] aUUIDs  = getArtifactUUIDs(context);

            oldAttrs = removeAttributes(oldAttrs, context);
            parser   = new AttributeParser(oldAttrs);

            CollectionAttribute newAttr = mergeAttributes(
                db, context, parser, aUUIDs);

            if (checkOutputSettings(newAttr, context)) {
                saveCollectionAttribute(db, context, newAttr);
            }

            helper.setAttribute(newAttr);

            if (aUUIDs != null) {
                for (String uuid: aUUIDs) {
                    helper.addArtifact(uuid);
                }
            }
        }
        catch (ArtifactDatabaseException ade) {
            log.error("Error while merging attribute documents.", ade);

            helper.setAttribute(parser.getCollectionAttribute());
        }

        return helper.toXML();
    }


    /**
     * Merge the current art:outputs nodes with the the outputs provided by the
     * artifacts in the Collection.
     *
     * @param uuids Artifact uuids.
     */
    protected CollectionAttribute mergeAttributes(
        ArtifactDatabase db,
        CallContext      context,
        AttributeParser  oldParser,
        String[]         uuids
    ) {
        CollectionAttribute cAttribute =
            buildOutAttributes(db, context, oldParser, uuids);

        if (cAttribute == null) {
            log.warn("mergeAttributes: cAttribute == null");
            return null;
        }

        cAttribute.setLoadedRecommendations(
            getLoadedRecommendations(oldParser.getAttributeDocument()));

        saveCollectionAttribute(db, context, cAttribute);

        return cAttribute;
    }


    protected Document removeAttributes(Document attrs, CallContext context) {
        Node outs = (Node) XMLUtils.xpath(
            attrs,
            "/art:attribute/art:outputs",
            XPathConstants.NODE,
            ArtifactNamespaceContext.INSTANCE);

        NodeList nodes = (NodeList) XMLUtils.xpath(
            attrs,
            "/art:attribute/art:outputs/art:output",
            XPathConstants.NODESET,
            ArtifactNamespaceContext.INSTANCE);

        if (nodes != null) {
            for (int i = 0; i < nodes.getLength(); i++) {
                Element e = (Element)nodes.item(i);
                if(!outputExists(e.getAttribute("name"), context)) {
                    outs.removeChild(e);
                }
            }
        }
        return attrs;
    }


    /**
     * True if current MasterArtifact has given output.
     * @param name Name of the output of interest.
     * @param context current context
     * @return true if current master artifact has given output.
     */
    protected boolean outputExists(String name, CallContext context) {
        FLYSArtifact master = getMasterArtifact(context);
        List<Output> outList = master.getOutputs(context);

        for (Output o : outList) {
            if (name.equals(o.getName())) {
                return true;
            }
        }
        return false;
    }

    /**
     * @param db The ArtifactDatabase which is required to save the attribute
     * into.
     * @param attribute The CollectionAttribute that should be stored in the
     * database.
     *
     * @return true, if the transaction was successful, otherwise false.
     */
    protected boolean saveCollectionAttribute(
        ArtifactDatabase    db,
        CallContext         context,
        CollectionAttribute attribute
    ) {
        log.info("Save new CollectionAttribute into database.");

        Document doc = attribute.toXML();

        try {
            // Save the merged document into database.
            db.setCollectionAttribute(identifier(), context.getMeta(), doc);

            log.info("Saving CollectionAttribute was successful.");

            return true;
        }
        catch (ArtifactDatabaseException adb) {
            log.error(adb, adb);
        }

        return false;
    }


    /**
     * Merge the recommendations which have already been loaded from the old
     * attribute document into the new attribute document. This is necessary,
     * because mergeAttributes() only merges the art:outputs nodes - all
     * other nodes are skipped.
     */
    protected Node getLoadedRecommendations(Document oldAttrs) {
        Element loadedRecoms = (Element) XMLUtils.xpath(
            oldAttrs,
            XPATH_LOADED_RECOMMENDATIONS,
            XPathConstants.NODE,
            ArtifactNamespaceContext.INSTANCE);

        return loadedRecoms;
    }


    /**
     * Evaluates the Output settings. If an Output has no Settings set, the
     * relevant OutGenerator is used to initialize a default Settings object.
     *
     * @param attribute The CollectionAttribute.
     * @param cc The CallContext.
     *
     * @return true, if the CollectionAttribute was modified, otherwise false.
     */
    protected boolean checkOutputSettings(
        CollectionAttribute attribute,
        CallContext         cc
    ) {
        boolean modified = false;

        Map<String, Output> outputMap = attribute != null
            ? attribute.getOutputs()
            : null;

        if (outputMap == null || outputMap.isEmpty()) {
            log.debug("No Output Settings check necessary.");
            return modified;
        }

        Set<Map.Entry<String, Output>> entries = outputMap.entrySet();

        for (Map.Entry<String, Output> entry: entries) {
            String outName = entry.getKey();
            Output output  = entry.getValue();

            if (outName.equals("sq_overview")) {
                continue;
            }
            Settings settings = output.getSettings();

            if (settings == null) {
                log.debug("No Settings set for Output '" + outName + "'.");
                output.setSettings(
                    createInitialOutputSettings(cc, attribute, outName));

                modified = true;
            }
        }

        return modified;
    }


    /**
     * This method uses the the OutGenerator for the specified Output
     * <i>out</i> to create an initial Settings object.
     *
     * @param cc The CallContext object.
     * @param attr The CollectionAttribute.
     * @param out The name of the output.
     *
     * @return a default Settings object for the specified Output.
     */
    protected Settings createInitialOutputSettings(
        CallContext         cc,
        CollectionAttribute attr,
        String              out
    ) {
        OutGenerator outGen = FLYSContext.getOutGenerator(cc, out, null);

        if (outGen == null) {
            return null;
        }

        // XXX NOTE: the outGen is not able to process its generate() operation,
        // because it has no OutputStream set!
        outGen.init(XMLUtils.newDocument(), null, cc);
        prepareMasterArtifact(outGen, cc);

        try {
            Document outAttr = getAttribute(cc, attr, out);
            OutputHelper helper = new OutputHelper(identifier());
            helper.doOut(outGen, out, out, outAttr, cc);
        }
        catch (ArtifactDatabaseException adbe) {
            log.error(adbe, adbe);
        }
        catch (IOException ioe) {
            log.error(ioe, ioe);
        }

        return outGen.getSettings();
    }


    @Override
    public void out(
        String       type,
        Document     format,
        OutputStream out,
        CallContext  context)
    throws IOException
    {
        boolean debug = log.isDebugEnabled();

        long reqBegin = System.currentTimeMillis();

        if (debug) {
            log.debug("FLYSArtifactCollection.out");
        }

        String name = XMLUtils.xpathString(
            format, XPATH_OUT_NAME, ArtifactNamespaceContext.INSTANCE);

        String subtype = XMLUtils.xpathString(
            format, XPATH_OUT_TYPE, ArtifactNamespaceContext.INSTANCE);

        if (debug) {
            log.debug("-> Output name = " + name);
            log.debug("-> Output type = " + type);
            log.debug("-> Output subtype = " + subtype);
        }

        OutGenerator generator = null;
        if (type != null
             && type.length() > 0
             && type.indexOf("chartinfo") > 0)
        {
            generator = FLYSContext.getOutGenerator(context, type, subtype);
        }
        else {
            generator = FLYSContext.getOutGenerator(context, name, subtype);
        }

        if (generator == null) {
            log.error("There is no generator specified for output: " + name);
            // TODO Throw an exception.

            return;
        }

        Document        oldAttrs  = getAttribute();
        AttributeParser parser    = new AttributeParser(oldAttrs);
        CollectionAttribute cAttr = parser.getCollectionAttribute();

        Output   output   = cAttr.getOutput(name);
        Settings settings = output.getSettings();

        generator.init(format, out, context);
        generator.setSettings(settings);
        generator.setCollection(this);
        prepareMasterArtifact(generator, context);

        try {
            Document attr = getAttribute(context, cAttr, name);
            OutputHelper helper = new OutputHelper(identifier());
            if (name.equals("sq_overview")) {
                helper.doOut(generator, name, subtype, format, context);
            }
            helper.doOut(generator, name, subtype, attr, context);
            generator.generate();
        }
        catch (ArtifactDatabaseException adbe) {
            log.error(adbe, adbe);
        }

        if (debug) {
            long duration = System.currentTimeMillis() -reqBegin;
            log.info("Processing out(" + name + ") took " + duration + " ms.");
        }
    }


    /**
     * Sets the master Artifact at the given <i>generator</i>.
     *
     * @param generator The generator that gets a master Artifact.
     * @param cc The CallContext.
     */
    protected void prepareMasterArtifact(OutGenerator generator, CallContext cc
    ) {
        // Get master artifact.
        FLYSArtifact master = getMasterArtifact(cc);
        if (master != null) {
            log.debug("Set master Artifact to uuid: " + master.identifier());
            generator.setMasterArtifact(master);
        }
        else {
            log.warn("Could not set master artifact.");
        }
    }


    /**
     * @return masterartifact or null if exception/not found.
     */
    protected FLYSArtifact getMasterArtifact(CallContext context)
    {
        try {
            ArtifactDatabase db = context.getDatabase();
            CallMeta callMeta   = context.getMeta();
            Document document   = db.getCollectionsMasterArtifact(
                identifier(), callMeta);

            String masterUUID   = XMLUtils.xpathString(
                document, XPATH_MASTER_UUID, ArtifactNamespaceContext.INSTANCE);
            FLYSArtifact masterArtifact =
                (FLYSArtifact) getArtifact(masterUUID, context);
            return masterArtifact;
        }
        catch (ArtifactDatabaseException ade) {
            log.error(ade, ade);
        }
        return null;
    }


    /**
     * Return merged output document.
     * @param uuids List of artifact uuids.
     */
    protected CollectionAttribute buildOutAttributes(
        ArtifactDatabase db,
        CallContext      context,
        AttributeParser  aParser,
        String[]         uuids)
    {
        FLYSContext flysContext = FLYSUtils.getFlysContext(context);
        StateEngine engine = (StateEngine) flysContext.get(
            FLYSContext.STATE_ENGINE_KEY);

        if (engine == null) {
            log.error("buildOutAttributes: engine == null");
            return null;
        }

        FLYSArtifact masterArtifact = getMasterArtifact(context);

        if (masterArtifact == null) {
            log.debug("buildOutAttributes: masterArtifact == null");
            return null;
        }

        OutputParser oParser = new OutputParser(db, context);

        if (uuids != null) {
            for (String uuid: uuids) {
                try {
                    oParser.parse(uuid);
                }
                catch (ArtifactDatabaseException ade) {
                    log.warn(ade, ade);
                }
            }
        }

        aParser.parse();

        AttributeWriter aWriter = new AttributeWriter(
            db,
            aParser.getCollectionAttribute(),
            aParser.getOuts(),
            aParser.getFacets(),
            oParser.getOuts(),
            oParser.getFacets(),
            engine.getCompatibleFacets(masterArtifact.getStateHistoryIds())
            );
        return aWriter.write();
    }


    /**
     * Returns the "attribute" (part of description document) for a specific
     * output type.
     *
     * @param context The CallContext object.
     * @param cAttr The CollectionAttribute.
     * @param output The name of the desired output type.
     *
     * @return the attribute for the desired output type.
     */
    protected Document getAttribute(
        CallContext         context,
        CollectionAttribute cAttr,
        String              output)
    throws    ArtifactDatabaseException
    {
        Document attr = cAttr.toXML();

        Map<String, String> vars = new HashMap<String, String>();
        vars.put("output", output);

        Node out = (Node) XMLUtils.xpath(
            attr,
            "art:attribute/art:outputs/art:output[@name=$output]",
            XPathConstants.NODE,
            ArtifactNamespaceContext.INSTANCE,
            vars);


        if (out != null) {
            Document o = XMLUtils.newDocument();

            o.appendChild(o.importNode(out, true));

            return o;
        }

        return null;
    }


    /**
     * This method returns the list of artifact UUIDs that this collections
     * contains.
     *
     * @param context The CallContext that is necessary to get information about
     * the ArtifactDatabase.
     *
     * @return a list of uuids.
     */
    protected String[] getArtifactUUIDs(CallContext context)
    throws    ArtifactDatabaseException
    {
        log.debug("FLYSArtifactCollection.getArtifactUUIDs");

        ArtifactDatabase db = context.getDatabase();
        CallMeta meta       = context.getMeta();

        Document itemList = db.listCollectionArtifacts(identifier(), meta);
        NodeList items    = (NodeList) XMLUtils.xpath(
            itemList,
            XPATH_COLLECTION_ITEMS,
            XPathConstants.NODESET,
            ArtifactNamespaceContext.INSTANCE);

        if (items == null || items.getLength() == 0) {
            log.debug("No artifacts found in this collection.");
            return null;
        }

        int num = items.getLength();

        List<String> uuids = new ArrayList<String>(num);

        for (int i = 0; i < num; i++) {
            String uuid = XMLUtils.xpathString(
                items.item(i),
                "@art:uuid",
                ArtifactNamespaceContext.INSTANCE);

            if (uuid != null && uuid.trim().length() != 0) {
                uuids.add(uuid);
            }
        }

        return uuids.toArray(new String[uuids.size()]);
    }


    /**
     * Returns a concrete Artifact of this collection specified by its uuid.
     *
     * @param uuid The Artifact's uuid.
     * @param context The CallContext.
     *
     * @return an Artifact.
     */
    protected Artifact getArtifact(String uuid, CallContext context)
    throws    ArtifactDatabaseException
    {
        log.debug("FLYSArtifactCollection.getArtifact");

        Backend backend               = Backend.getInstance();
        PersistentArtifact persistent = backend.getArtifact(uuid);

        return persistent != null ? persistent.getArtifact() : null;
    }


}
// vim:set ts=4 sw=4 si et sta sts=4 fenc=utf8 :

http://dive4elements.wald.intevation.org