org.dspace.app.util.SubmissionConfigReader.java Source code

Java tutorial

Introduction

Here is the source code for org.dspace.app.util.SubmissionConfigReader.java

Source

/**
 * The contents of this file are subject to the license and copyright
 * detailed in the LICENSE and NOTICE files at the root of the source
 * tree and available online at
 *
 * http://www.dspace.org/license/
 */
package org.dspace.app.util;

import java.io.File;
import java.util.*;
import javax.servlet.ServletException;
import org.xml.sax.SAXException;
import org.w3c.dom.*;
import javax.xml.parsers.*;

import org.apache.commons.lang3.StringUtils;
import org.apache.log4j.Logger;
import org.dspace.services.factory.DSpaceServicesFactory;

/**
 * Item Submission configuration generator for DSpace. Reads and parses the
 * installed submission process configuration file, item-submission.xml, from
 * the configuration directory. This submission process definition details the
 * ordering of the steps (and number of steps) that occur during the Item
 * Submission Process. There may be multiple Item Submission processes defined,
 * where each definition is assigned a unique name.
 * 
 * The file also specifies which collections use which Item Submission process.
 * At a minimum, the definitions file must define a default mapping from the
 * placeholder collection # to the distinguished submission process 'default'.
 * Any collections that use a custom submission process are listed paired with
 * the name of the item submission process they use.
 * 
 * @see org.dspace.app.util.SubmissionConfig
 * @see org.dspace.app.util.SubmissionStepConfig
 * 
 * @author Tim Donohue based on DCInputsReader by Brian S. Hughes
 * @version $Revision$
 */

public class SubmissionConfigReader {
    /**
     * The ID of the default collection. Will never be the ID of a named
     * collection
     */
    public static final String DEFAULT_COLLECTION = "default";

    /** Prefix of the item submission definition XML file */
    static final String SUBMIT_DEF_FILE_PREFIX = "item-submission";

    /** Suffix of the item submission definition XML file */
    static final String SUBMIT_DEF_FILE_SUFFIX = ".xml";

    /** log4j logger */
    private static Logger log = Logger.getLogger(SubmissionConfigReader.class);

    /** The fully qualified pathname of the directory containing the Item Submission Configuration file */
    private String configDir = DSpaceServicesFactory.getInstance().getConfigurationService()
            .getProperty("dspace.dir") + File.separator + "config" + File.separator;

    /**
     * Hashmap which stores which submission process configuration is used by
     * which collection, computed from the item submission config file
     * (specifically, the 'submission-map' tag)
     */
    private Map<String, String> collectionToSubmissionConfig = null;

    /**
     * Reference to the global submission step definitions defined in the
     * "step-definitions" section
     */
    private Map<String, Map<String, String>> stepDefns = null;

    /**
     * Reference to the item submission definitions defined in the
     * "submission-definitions" section
     */
    private Map<String, List<Map<String, String>>> submitDefns = null;

    /**
     * Mini-cache of last SubmissionConfig object requested (so that we don't
     * always reload from scratch)
     */
    private SubmissionConfig lastSubmissionConfig = null;

    /**
     * Load Submission Configuration from the
     * item-submission.xml configuration file 
     * @throws SubmissionConfigReaderException if servlet error
     */
    public SubmissionConfigReader() throws SubmissionConfigReaderException {
        buildInputs(configDir + SUBMIT_DEF_FILE_PREFIX + SUBMIT_DEF_FILE_SUFFIX);
    }

    public void reload() throws SubmissionConfigReaderException {
        collectionToSubmissionConfig = null;
        stepDefns = null;
        submitDefns = null;
        buildInputs(configDir + SUBMIT_DEF_FILE_PREFIX + SUBMIT_DEF_FILE_SUFFIX);
    }

    /**
     * Parse an XML encoded item submission configuration file.
     * <P>
     * Creates two main hashmaps:
     * <ul>
     * <li>Hashmap of Collection to Submission definition mappings -
     * defines which Submission process a particular collection uses
     * <li>Hashmap of all Submission definitions.  List of all valid
     * Submision Processes by name.
     * </ul>
     */
    private void buildInputs(String fileName) throws SubmissionConfigReaderException {
        collectionToSubmissionConfig = new HashMap<String, String>();
        submitDefns = new HashMap<String, List<Map<String, String>>>();

        String uri = "file:" + new File(fileName).getAbsolutePath();

        try {
            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
            factory.setValidating(false);
            factory.setIgnoringComments(true);
            factory.setIgnoringElementContentWhitespace(true);

            DocumentBuilder db = factory.newDocumentBuilder();
            Document doc = db.parse(uri);
            doNodes(doc);
        } catch (FactoryConfigurationError fe) {
            throw new SubmissionConfigReaderException("Cannot create Item Submission Configuration parser", fe);
        } catch (Exception e) {
            throw new SubmissionConfigReaderException("Error creating Item Submission Configuration: " + e);
        }
    }

    /**
     * 
     * @return the name of the default submission configuration
     */
    public String getDefaultSubmissionConfigName() {
        return collectionToSubmissionConfig.get(DEFAULT_COLLECTION);
    }

    /**
     * Returns all the Item Submission process configs with pagination 
     * 
     * @param limit
     *            max number of SubmissionConfig to return
     * @param offset
     *            number of SubmissionConfig to skip in the return
     *            
     * @return the list of SubmissionConfig 
     * 
     */
    public List<SubmissionConfig> getAllSubmissionConfigs(Integer limit, Integer offset) {
        int idx = 0;
        int count = 0;
        List<SubmissionConfig> subConfigs = new LinkedList<SubmissionConfig>();
        for (String key : submitDefns.keySet()) {
            if (offset == null || idx >= offset) {
                count++;
                subConfigs.add(getSubmissionConfigByName(key));
            }
            idx++;
            if (count >= limit) {
                break;
            }
        }
        return subConfigs;
    }

    public int countSubmissionConfigs() {
        return submitDefns.size();
    }

    /**
     * Returns the Item Submission process config used for a particular
     * collection, or the default if none is defined for the collection
     * 
     * @param collectionHandle
     *            collection's unique Handle
     * @return the SubmissionConfig representing the item submission config
     * 
     * @throws SubmissionConfigReaderException
     *             if no default submission process configuration defined
     */
    public SubmissionConfig getSubmissionConfigByCollection(String collectionHandle) {
        // get the name of the submission process config for this collection
        String submitName = collectionToSubmissionConfig.get(collectionHandle);
        if (submitName == null) {
            submitName = collectionToSubmissionConfig.get(DEFAULT_COLLECTION);
        }
        if (submitName == null) {
            throw new IllegalStateException(
                    "No item submission process configuration designated as 'default' in 'submission-map' section of 'item-submission.xml'.");
        }
        return getSubmissionConfigByName(submitName);
    }

    /**
     * Returns the Item Submission process config 
     * 
     * @param submitName
     *            submission process unique name
     * @return the SubmissionConfig representing the item submission config
     * 
     */
    public SubmissionConfig getSubmissionConfigByName(String submitName) {
        log.debug("Loading submission process config named '" + submitName + "'");

        // check mini-cache, and return if match
        if (lastSubmissionConfig != null && lastSubmissionConfig.getSubmissionName().equals(submitName)) {
            log.debug("Found submission process config '" + submitName + "' in cache.");

            return lastSubmissionConfig;
        }

        // cache miss - construct new SubmissionConfig
        List<Map<String, String>> steps = submitDefns.get(submitName);

        if (steps == null) {
            throw new IllegalStateException("Missing the Item Submission process config '" + submitName
                    + "' (or unable to load) from 'item-submission.xml'.");
        }

        log.debug("Submission process config '" + submitName + "' not in cache. Reloading from scratch.");

        lastSubmissionConfig = new SubmissionConfig(
                StringUtils.equals(getDefaultSubmissionConfigName(), submitName), submitName, steps);

        log.debug("Submission process config has " + lastSubmissionConfig.getNumberOfSteps() + " steps listed.");

        return lastSubmissionConfig;
    }

    /**
     * Returns a particular global step definition based on its ID.
     * <P>
     * Global step definitions are those defined in the {@code <step-definitions>}
     * section of the configuration file.
     * 
     * @param stepID
     *            step's identifier
     * 
     * @return the SubmissionStepConfig representing the step
     * 
     * @throws SubmissionConfigReaderException
     *             if no default submission process configuration defined
     */
    public SubmissionStepConfig getStepConfig(String stepID) throws SubmissionConfigReaderException {
        // We should already have the step definitions loaded
        if (stepDefns != null) {
            // retreive step info
            Map<String, String> stepInfo = stepDefns.get(stepID);

            if (stepInfo != null) {
                return new SubmissionStepConfig(stepInfo);
            }
        }

        return null;
    }

    /**
     * Process the top level child nodes in the passed top-level node. These
     * should correspond to the collection-form maps, the form definitions, and
     * the display/storage word pairs.
     */
    private void doNodes(Node n) throws SAXException, SubmissionConfigReaderException {
        if (n == null) {
            return;
        }
        Node e = getElement(n);
        NodeList nl = e.getChildNodes();
        int len = nl.getLength();
        boolean foundMap = false;
        boolean foundStepDefs = false;
        boolean foundSubmitDefs = false;
        for (int i = 0; i < len; i++) {
            Node nd = nl.item(i);
            if ((nd == null) || isEmptyTextNode(nd)) {
                continue;
            }
            String tagName = nd.getNodeName();
            if (tagName.equals("submission-map")) {
                processMap(nd);
                foundMap = true;
            } else if (tagName.equals("step-definitions")) {
                processStepDefinition(nd);
                foundStepDefs = true;
            } else if (tagName.equals("submission-definitions")) {
                processSubmissionDefinition(nd);
                foundSubmitDefs = true;
            }
            // Ignore unknown nodes
        }
        if (!foundMap) {
            throw new SubmissionConfigReaderException(
                    "No collection to item submission map ('submission-map') found in 'item-submission.xml'");
        }
        if (!foundStepDefs) {
            throw new SubmissionConfigReaderException(
                    "No 'step-definitions' section found in 'item-submission.xml'");
        }
        if (!foundSubmitDefs) {
            throw new SubmissionConfigReaderException(
                    "No 'submission-definitions' section found in 'item-submission.xml'");
        }
    }

    /**
     * Process the submission-map section of the XML file. Each element looks
     * like: <name-map collection-handle="hdl" submission-name="name" /> Extract
     * the collection handle and item submission name, put name in hashmap keyed
     * by the collection handle.
     */
    private void processMap(Node e) throws SAXException {
        NodeList nl = e.getChildNodes();
        int len = nl.getLength();
        for (int i = 0; i < len; i++) {
            Node nd = nl.item(i);
            if (nd.getNodeName().equals("name-map")) {
                String id = getAttribute(nd, "collection-handle");
                String value = getAttribute(nd, "submission-name");
                String content = getValue(nd);
                if (id == null) {
                    throw new SAXException(
                            "name-map element is missing collection-handle attribute in 'item-submission.xml'");
                }
                if (value == null) {
                    throw new SAXException(
                            "name-map element is missing submission-name attribute in 'item-submission.xml'");
                }
                if (content != null && content.length() > 0) {
                    throw new SAXException(
                            "name-map element has content in 'item-submission.xml', it should be empty.");
                }
                collectionToSubmissionConfig.put(id, value);
            } // ignore any child node that isn't a "name-map"
        }
    }

    /**
     * Process the "step-definition" section of the XML file. Each element is
     * formed thusly: <step id="unique-id"> ...step_fields... </step> The valid
     * step_fields are: heading, processing-servlet.
     * <P>
     * Extract the step information (from the step_fields) and place in a
     * HashMap whose key is the step's unique id.
     */
    private void processStepDefinition(Node e) throws SAXException, SubmissionConfigReaderException {
        stepDefns = new HashMap<String, Map<String, String>>();

        NodeList nl = e.getChildNodes();
        int len = nl.getLength();
        for (int i = 0; i < len; i++) {
            Node nd = nl.item(i);
            // process each step definition
            if (nd.getNodeName().equals("step")) {
                String stepID = getAttribute(nd, "id");
                if (stepID == null) {
                    throw new SAXException(
                            "step element has no 'id' attribute in 'item-submission.xml', which is required in the 'step-definitions' section");
                } else if (stepDefns.containsKey(stepID)) {
                    throw new SAXException(
                            "There are two step elements with the id '" + stepID + "' in 'item-submission.xml'");
                }

                Map<String, String> stepInfo = processStepChildNodes("step-definition", nd);

                stepDefns.put(stepID, stepInfo);
            } // ignore any child that is not a 'step'
        }

        // Sanity check number of step definitions
        if (stepDefns.size() < 1) {
            throw new SubmissionConfigReaderException(
                    "step-definition section has no steps! A step with id='collection' is required in 'item-submission.xml'!");
        }

    }

    /**
     * Process the "submission-definition" section of the XML file. Each element
     * is formed thusly: <submission-process name="submitName">...steps...</submit-process>
     * Each step subsection is formed: <step> ...step_fields... </step> (with
     * optional "id" attribute, to reference a step from the <step-definition>
     * section). The valid step_fields are: heading, class-name.
     * <P>
     * Extract the submission-process name and steps and place in a HashMap
     * whose key is the submission-process's unique name.
     */
    private void processSubmissionDefinition(Node e) throws SAXException, SubmissionConfigReaderException {
        int numSubmitProcesses = 0;
        List<String> submitNames = new ArrayList<String>();

        // find all child nodes of the 'submission-definition' node and loop
        // through
        NodeList nl = e.getChildNodes();
        int len = nl.getLength();
        for (int i = 0; i < len; i++) {
            Node nd = nl.item(i);

            // process each 'submission-process' node
            if (nd.getNodeName().equals("submission-process")) {
                numSubmitProcesses++;
                String submitName = getAttribute(nd, "name");
                if (submitName == null) {
                    throw new SAXException(
                            "'submission-process' element has no 'name' attribute in 'item-submission.xml'");
                } else if (submitNames.contains(submitName)) {
                    throw new SAXException("There are two 'submission-process' elements with the name '"
                            + submitName + "' in 'item-submission.xml'.");
                }
                submitNames.add(submitName);

                // the 'submission-process' definition contains steps
                List<Map<String, String>> steps = new ArrayList<Map<String, String>>();
                submitDefns.put(submitName, steps);

                // loop through all the 'step' nodes of the 'submission-process'
                NodeList pl = nd.getChildNodes();
                int lenStep = pl.getLength();
                for (int j = 0; j < lenStep; j++) {
                    Node nStep = pl.item(j);

                    // process each 'step' definition
                    if (nStep.getNodeName().equals("step")) {
                        // check for an 'id' attribute
                        String stepID = getAttribute(nStep, "id");

                        Map<String, String> stepInfo;

                        // if this step has an id, load its information from the
                        // step-definition section
                        if ((stepID != null) && (stepID.length() > 0)) {
                            if (stepDefns.containsKey(stepID)) {
                                // load the step information from the
                                // step-definition
                                stepInfo = stepDefns.get(stepID);
                            } else {
                                throw new SubmissionConfigReaderException("The Submission process config named "
                                        + submitName + " contains a step with id=" + stepID
                                        + ".  There is no step with this 'id' defined in the 'step-definition' section of 'item-submission.xml'.");
                            }

                            // Ignore all children of a step element with an
                            // "id"
                        } else {
                            // get information about step from its children
                            // nodes
                            stepInfo = processStepChildNodes("submission-process", nStep);
                        }

                        steps.add(stepInfo);

                    } // ignore any child that is not a 'step'
                }

                // sanity check number of steps
                if (steps.size() < 1) {
                    throw new SubmissionConfigReaderException("Item Submission process config named " + submitName
                            + " has no steps defined in 'item-submission.xml'");
                }

            }
        }
        if (numSubmitProcesses == 0) {
            throw new SubmissionConfigReaderException(
                    "No 'submission-process' elements/definitions found in 'item-submission.xml'");
        }
    }

    /**
     * Process the children of the "step" tag of the XML file. Returns a HashMap
     * of all the fields under that "step" tag, where the key is the field name,
     * and the value is the field value.
     * 
     */
    private Map<String, String> processStepChildNodes(String configSection, Node nStep)
            throws SubmissionConfigReaderException {
        // initialize the HashMap of step Info
        Map<String, String> stepInfo = new HashMap<String, String>();

        NodeList flds = nStep.getChildNodes();
        int lenflds = flds.getLength();
        for (int k = 0; k < lenflds; k++) {
            // process each child node of a <step> tag
            Node nfld = flds.item(k);

            String tagName = nfld.getNodeName();
            if (!isEmptyTextNode(nfld)) {
                String value = getValue(nfld);
                stepInfo.put(tagName, value);
            }

            for (int idx = 0; idx < nfld.getAttributes().getLength(); idx++) {
                Node nAttr = nfld.getAttributes().item(idx);
                String attrName = nAttr.getNodeName();
                String attrValue = nAttr.getNodeValue();
                stepInfo.put(tagName + "." + attrName, attrValue);
            }
        } // end for each field

        // check for ID attribute & save to step info
        String stepID = getAttribute(nStep, "id");
        if (StringUtils.isNotBlank(stepID)) {
            stepInfo.put("id", stepID);
        }

        String mandatory = getAttribute(nStep, "mandatory");
        if (StringUtils.isNotBlank(mandatory)) {
            stepInfo.put("mandatory", mandatory);
        }

        // look for REQUIRED 'step' information
        String missing = null;
        if (stepInfo.get("processing-class") == null) {
            missing = "'processing-class'";
        }
        if (missing != null) {
            String msg = "Required field " + missing + " missing in a 'step' in the " + configSection
                    + " of the item submission configuration file ('item-submission.xml')";
            throw new SubmissionConfigReaderException(msg);
        }

        return stepInfo;
    }

    private Node getElement(Node nd) {
        NodeList nl = nd.getChildNodes();
        int len = nl.getLength();
        for (int i = 0; i < len; i++) {
            Node n = nl.item(i);
            if (n.getNodeType() == Node.ELEMENT_NODE) {
                return n;
            }
        }
        return null;
    }

    private boolean isEmptyTextNode(Node nd) {
        boolean isEmpty = false;
        if (nd.getNodeType() == Node.TEXT_NODE) {
            String text = nd.getNodeValue().trim();
            if (text.length() == 0) {
                isEmpty = true;
            }
        }
        return isEmpty;
    }

    /**
     * Returns the value of the node's attribute named <name>
     */
    private String getAttribute(Node e, String name) {
        NamedNodeMap attrs = e.getAttributes();
        int len = attrs.getLength();
        if (len > 0) {
            int i;
            for (i = 0; i < len; i++) {
                Node attr = attrs.item(i);
                if (name.equals(attr.getNodeName())) {
                    return attr.getNodeValue().trim();
                }
            }
        }
        // no such attribute
        return null;
    }

    /**
     * Returns the value found in the Text node (if any) in the node list that's
     * passed in.
     */
    private String getValue(Node nd) {
        NodeList nl = nd.getChildNodes();
        int len = nl.getLength();
        for (int i = 0; i < len; i++) {
            Node n = nl.item(i);
            short type = n.getNodeType();
            if (type == Node.TEXT_NODE) {
                return n.getNodeValue().trim();
            }
        }
        // Didn't find a text node
        return null;
    }
}