/* * SubmissionConfigReader.java * * Version: $Revision: 3734 $ * * Date: $Date: 2009-04-24 04:00:19 +0000 (Fri, 24 Apr 2009) $ * * Copyright (c) 2002-2009, The DSpace Foundation. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * - Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * - Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * - Neither the name of the DSpace Foundation nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH * DAMAGE. */ package org.dspace.app.util; import java.io.File; import java.util.Vector; import java.util.HashMap; import java.util.Map; import java.lang.Exception; import javax.servlet.ServletException; import org.xml.sax.SAXException; import org.w3c.dom.*; import javax.xml.parsers.*; import org.apache.log4j.Logger; import org.dspace.core.ConfigurationManager; /** * 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 definiton 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: 3734 $ */ 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 = ConfigurationManager.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 HashMap collectionToSubmissionConfig = null; /** * Reference to the global submission step definitions defined in the * "step-definitions" section */ private HashMap stepDefns = null; /** * Reference to the item submission definitions defined in the * "submission-definitions" section */ private HashMap 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 */ public SubmissionConfigReader() throws ServletException { 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 ServletException { collectionToSubmissionConfig = new HashMap(); submitDefns = new HashMap(); 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 ServletException( "Cannot create Item Submission Configuration parser", fe); } catch (Exception e) { throw new ServletException( "Error creating Item Submission Configuration: " + e); } } /** * 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 * @param isWorkflow * whether or not we are loading the submission process for a * workflow * @return the SubmissionConfig representing the item submission config * * @throws ServletException * if no default submission process configuration defined */ public SubmissionConfig getSubmissionConfig(String collectionHandle, boolean isWorkflow) throws ServletException { // get the name of the submission process config for this collection String submitName = (String) collectionToSubmissionConfig .get(collectionHandle); if (submitName == null) { submitName = (String) collectionToSubmissionConfig .get(DEFAULT_COLLECTION); } if (submitName == null) { throw new ServletException( "No item submission process configuration designated as 'default' in 'submission-map' section of 'item-submission.xml'."); } log.debug("Loading submission process config named '" + submitName + "'"); // check mini-cache, and return if match if (lastSubmissionConfig != null && lastSubmissionConfig.getSubmissionName().equals(submitName) && lastSubmissionConfig.isWorkflow() == isWorkflow) { log.debug("Found submission process config '" + submitName + "' in cache."); return lastSubmissionConfig; } // cache miss - construct new SubmissionConfig Vector steps = (Vector) submitDefns.get(submitName); if (steps == null) { throw new ServletException( "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(submitName, steps, isWorkflow); 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 <step-definitions> * section of the configuration file. * * @param stepID * step's identifier * * @return the SubmissionStepConfig representing the step * * @throws ServletException * if no default submission process configuration defined */ public SubmissionStepConfig getStepConfig(String stepID) throws ServletException { // We should already have the step definitions loaded if (stepDefns != null) { // retreive step info Map stepInfo = (Map) 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, ServletException { 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 ServletException( "No collection to item submission map ('submission-map') found in 'item-submission.xml'"); } if (!foundStepDefs) { throw new ServletException("No 'step-definitions' section found in 'item-submission.xml'"); } if (!foundSubmitDefs) { throw new ServletException( "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, ServletException { int numStepDefns = 0; stepDefns = new HashMap(); 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")) { numStepDefns++; 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'"); } HashMap 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 ServletException( "step-definition section has no steps! A step with id='collection' is required in 'item-submission.xml'!"); } // Sanity check to see that the required "collection" step is defined if (!stepDefns.containsKey(SubmissionStepConfig.SELECT_COLLECTION_STEP)) { throw new ServletException( "The step-definition section is REQUIRED to have a step with id='" + SubmissionStepConfig.SELECT_COLLECTION_STEP + "' in 'item-submission.xml'! This step is used to ensure that a new item submission is assigned to a collection."); } // Sanity check to see that the required "complete" step is defined if (!stepDefns.containsKey(SubmissionStepConfig.COMPLETE_STEP)) { throw new ServletException( "The step-definition section is REQUIRED to have a step with id='" + SubmissionStepConfig.COMPLETE_STEP + "' in 'item-submission.xml'! This step is used to perform all processing necessary at the completion of the submission (e.g. starting workflow)."); } } /** * 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, ServletException { int numSubmitProcesses = 0; Vector submitNames = new Vector(); // 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 Vector steps = new Vector(); submitDefns.put(submitName, steps); // loop through all the 'step' nodes of the 'submission-process' int stepNum = 0; NodeList pl = nd.getChildNodes(); int lenStep = pl.getLength(); for (int j = 0; j < lenStep; j++) { Node nStep = pl.item(j); stepNum++; // process each 'step' definition if (nStep.getNodeName().equals("step")) { // check for an 'id' attribute String stepID = getAttribute(nStep, "id"); HashMap 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 = (HashMap) stepDefns.get(stepID); } else { throw new SAXException( "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 ServletException( "Item Submission process config named " + submitName + " has no steps defined in 'item-submission.xml'"); } // ALL Item Submission processes MUST BEGIN with selecting a // Collection. So, automatically insert in the "collection" step // (from the 'step-definition' section) // Note: we already did a sanity check that this "collection" // step exists. steps.add(0, (HashMap) stepDefns .get(SubmissionStepConfig.SELECT_COLLECTION_STEP)); // ALL Item Submission processes MUST END with the // "Complete" processing step. // So, automatically append in the "complete" step // (from the 'step-definition' section) // Note: we already did a sanity check that this "complete" // step exists. steps.add((HashMap) stepDefns .get(SubmissionStepConfig.COMPLETE_STEP)); } } if (numSubmitProcesses == 0) { throw new ServletException( "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 HashMap processStepChildNodes(String configSection, Node nStep) throws SAXException, ServletException { // initialize the HashMap of step Info HashMap stepInfo = new HashMap(); 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); if (!isEmptyTextNode(nfld)) { String tagName = nfld.getNodeName(); String value = getValue(nfld); stepInfo.put(tagName, value); } }// end for each field // check for ID attribute & save to step info String stepID = getAttribute(nStep, "id"); if (stepID != null && stepID.length() > 0) { stepInfo.put("id", stepID); } // 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 SAXException(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; } }