org.fosstrak.epcis.repository.capture.CaptureOperationsModule.java Source code

Java tutorial

Introduction

Here is the source code for org.fosstrak.epcis.repository.capture.CaptureOperationsModule.java

Source

/*
 * Copyright (C) 2007 ETH Zurich
 *
 * This file is part of Fosstrak (www.fosstrak.org).
 *
 * Fosstrak is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License version 2.1, as published by the Free Software Foundation.
 *
 * Fosstrak is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with Fosstrak; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 * Boston, MA  02110-1301  USA
 */

package org.fosstrak.epcis.repository.capture;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.security.Principal;
import java.sql.SQLException;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import javax.xml.validation.Validator;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.fosstrak.epcis.repository.EpcisConstants;
import org.fosstrak.epcis.repository.InternalBusinessException;
import org.fosstrak.epcis.repository.InvalidFormatException;
import org.fosstrak.epcis.repository.model.Action;
import org.fosstrak.epcis.repository.model.AggregationEvent;
import org.fosstrak.epcis.repository.model.BaseEvent;
import org.fosstrak.epcis.repository.model.BusinessLocationAttrId;
import org.fosstrak.epcis.repository.model.BusinessLocationId;
import org.fosstrak.epcis.repository.model.BusinessStepAttrId;
import org.fosstrak.epcis.repository.model.BusinessStepId;
import org.fosstrak.epcis.repository.model.BusinessTransaction;
import org.fosstrak.epcis.repository.model.BusinessTransactionAttrId;
import org.fosstrak.epcis.repository.model.BusinessTransactionId;
import org.fosstrak.epcis.repository.model.BusinessTransactionTypeAttrId;
import org.fosstrak.epcis.repository.model.BusinessTransactionTypeId;
import org.fosstrak.epcis.repository.model.DispositionAttrId;
import org.fosstrak.epcis.repository.model.DispositionId;
import org.fosstrak.epcis.repository.model.EPCClass;
import org.fosstrak.epcis.repository.model.EPCClassAttrId;
import org.fosstrak.epcis.repository.model.EventFieldExtension;
import org.fosstrak.epcis.repository.model.ObjectEvent;
import org.fosstrak.epcis.repository.model.QuantityEvent;
import org.fosstrak.epcis.repository.model.ReadPointAttrId;
import org.fosstrak.epcis.repository.model.ReadPointId;
import org.fosstrak.epcis.repository.model.TransactionEvent;
import org.fosstrak.epcis.repository.model.VocabularyAttrCiD;
import org.fosstrak.epcis.repository.model.VocabularyAttributeElement;
import org.fosstrak.epcis.repository.model.VocabularyElement;
import org.fosstrak.epcis.utils.TimeParser;
import org.hibernate.Criteria;
import org.hibernate.ObjectNotFoundException;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;
import org.hibernate.criterion.Restrictions;
import org.w3c.dom.DOMException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.ErrorHandler;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;

/**
 * CaptureOperationsModule implements the core capture operations. Converts XML
 * events delivered by HTTP POST into SQL and inserts them into the database.
 * <p>
 * TODO: the parsing of the xml inputstream should be done in the
 * CaptureOperationsServlet; this class should implement EpcisCaptureInterface
 * such that CaptureOperationsServlet can call its capture method and provide it
 * with the parsed events.
 * 
 * @author David Gubler
 * @author Alain Remund
 * @author Marco Steybe
 * @author Nikos Kefalakis (nkef)
 */
public class CaptureOperationsModule {

    private static final Log LOG = LogFactory.getLog(CaptureOperationsModule.class);

    private static final Map<String, Class<?>> vocClassMap = new HashMap<String, Class<?>>();

    static {
        vocClassMap.put(EpcisConstants.BUSINESS_LOCATION_ID, BusinessLocationId.class);
        vocClassMap.put(EpcisConstants.BUSINESS_STEP_ID, BusinessStepId.class);
        vocClassMap.put(EpcisConstants.BUSINESS_TRANSACTION_ID, BusinessTransactionId.class);
        vocClassMap.put(EpcisConstants.BUSINESS_TRANSACTION_TYPE_ID, BusinessTransactionTypeId.class);
        vocClassMap.put(EpcisConstants.DISPOSITION_ID, DispositionId.class);
        vocClassMap.put(EpcisConstants.EPC_CLASS_ID, EPCClass.class);
        vocClassMap.put(EpcisConstants.READ_POINT_ID, ReadPointId.class);
    }

    // (nkef) Added to support the Master Data Capture I/F
    private static final Map<String, Class<?>> vocAttributeClassMap = new HashMap<String, Class<?>>();
    private static final Map<String, String> vocAttributeTablesMap = new HashMap<String, String>();
    private static Map<String, String> vocabularyTablenameMap = new HashMap<String, String>();

    static {
        vocAttributeClassMap.put(EpcisConstants.BUSINESS_LOCATION_ID, BusinessLocationAttrId.class);
        vocAttributeClassMap.put(EpcisConstants.BUSINESS_STEP_ID, BusinessStepAttrId.class);
        vocAttributeClassMap.put(EpcisConstants.BUSINESS_TRANSACTION_ID, BusinessTransactionAttrId.class);
        vocAttributeClassMap.put(EpcisConstants.BUSINESS_TRANSACTION_TYPE_ID, BusinessTransactionTypeAttrId.class);
        vocAttributeClassMap.put(EpcisConstants.DISPOSITION_ID, DispositionAttrId.class);
        vocAttributeClassMap.put(EpcisConstants.EPC_CLASS_ID, EPCClassAttrId.class);
        vocAttributeClassMap.put(EpcisConstants.READ_POINT_ID, ReadPointAttrId.class);

        vocAttributeTablesMap.put(EpcisConstants.BUSINESS_LOCATION_ID, "voc_BizLoc_attr");
        vocAttributeTablesMap.put(EpcisConstants.BUSINESS_STEP_ID, "voc_BizStep_attr");
        vocAttributeTablesMap.put(EpcisConstants.BUSINESS_TRANSACTION_ID, "voc_BizTrans_attr");
        vocAttributeTablesMap.put(EpcisConstants.BUSINESS_TRANSACTION_TYPE_ID, "voc_BizTransType_attr");
        vocAttributeTablesMap.put(EpcisConstants.DISPOSITION_ID, "voc_Disposition_attr");
        vocAttributeTablesMap.put(EpcisConstants.EPC_CLASS_ID, "voc_EPCClass_attr");
        vocAttributeTablesMap.put(EpcisConstants.READ_POINT_ID, "voc_ReadPoint_attr");

        vocabularyTablenameMap.put(EpcisConstants.BUSINESS_STEP_ID, "voc_BizStep");
        vocabularyTablenameMap.put(EpcisConstants.BUSINESS_LOCATION_ID, "voc_BizLoc");
        vocabularyTablenameMap.put(EpcisConstants.BUSINESS_TRANSACTION_ID, "voc_BizTrans");
        vocabularyTablenameMap.put(EpcisConstants.BUSINESS_TRANSACTION_TYPE_ID, "voc_BizTransType");
        vocabularyTablenameMap.put(EpcisConstants.DISPOSITION_ID, "voc_Disposition");
        vocabularyTablenameMap.put(EpcisConstants.EPC_CLASS_ID, "voc_EPCClass");
        vocabularyTablenameMap.put(EpcisConstants.READ_POINT_ID, "voc_ReadPoint");

    }

    /**
     * The XSD schema which validates the incoming messages.
     */
    private Schema schema;

    /**
     * The XSD schema which validates the MasterData incoming messages.(nkef)
     */
    private Schema masterDataSchema;

    /**
     * Whether we should insert new vocabulary or throw an error message.
     */
    private boolean insertMissingVoc = true;

    /**
     * Whether the dbReset operation is allowed or not.
     */
    private boolean dbResetAllowed = false;

    /**
     * The SQL files to be executed when the dbReset operation is invoked.
     */
    private List<File> dbResetScripts = null;

    /**
     * Interface to the database.
     */
    private SessionFactory sessionFactory;

    /**
     * Initializes the EPCIS schema used for validating incoming capture
     * requests. Loads the WSDL and XSD files from the classpath (the schema is
     * bundled with epcis-commons.jar).
     * 
     * @return An instantiated schema validation object.
     */
    private Schema initEpcisSchema(String xsdFile) {
        InputStream is = this.getClass().getResourceAsStream(xsdFile);
        if (is != null) {
            try {
                SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
                Source schemaSrc = new StreamSource(is);
                schemaSrc.setSystemId(CaptureOperationsServlet.class.getResource(xsdFile).toString());
                Schema schema = schemaFactory.newSchema(schemaSrc);
                LOG.debug("EPCIS schema file initialized and loaded successfully");
                return schema;
            } catch (Exception e) {
                LOG.warn("Unable to load or parse the EPCIS schema", e);
            }
        } else {
            LOG.error("Unable to load the EPCIS schema file from classpath: cannot find resource " + xsdFile);
        }
        LOG.warn("Schema validation will not be available!");
        return null;
    }

    /**
     * Resets the database.
     * 
     * @throws SQLException
     *             If something goes wrong resetting the database.
     * @throws IOException
     *             If something goes wrong reading the reset script.
     * @throws UnsupportedOperationsException
     *             If database resets are not allowed.
     */
    public void doDbReset() throws SQLException, IOException, UnsupportedOperationException {
        if (dbResetAllowed) {
            if (dbResetScripts == null || dbResetScripts.isEmpty()) {
                LOG.warn("dbReset operation invoked but no dbReset script is configured!");
            } else {
                Session session = null;
                try {
                    session = sessionFactory.openSession();
                    Transaction tx = null;
                    for (File file : dbResetScripts) {
                        try {
                            tx = session.beginTransaction();
                            LOG.info("Running db reset script from file " + file);
                            BufferedReader reader = new BufferedReader(new FileReader(file));
                            String line;
                            String sql = "";
                            while ((line = reader.readLine()) != null) {
                                if (!line.startsWith("--") && StringUtils.isNotBlank(line)) {
                                    sql += line;
                                    if (sql.endsWith(";")) {
                                        LOG.debug("SQL: " + sql);
                                        session.createSQLQuery(sql).executeUpdate();
                                        sql = "";
                                    }
                                }
                            }
                            tx.commit();
                        } catch (Throwable e) {
                            LOG.error("dbReset failed for " + file + ": " + e.toString(), e);
                            if (tx != null) {
                                tx.rollback();
                            }
                            throw new SQLException(e.toString());
                        }
                    }
                } finally {
                    if (session != null) {
                        session.close();
                    }
                }
            }
        } else {
            throw new UnsupportedOperationException();
        }
    }

    /**
     * Implements the EPCIS capture operation. Takes an input stream, extracts
     * the payload into an XML document, validates the document against the
     * EPCIS schema, and captures the EPCIS events given in the document.
     * 
     * @throws IOException
     *             If an error occurred while validating the request or writing
     *             the response.
     * @throws ParserConfigurationException
     * @throws SAXException
     *             If the XML document is malformed or invalid
     * @throws InvalidFormatException
     */
    public void doCapture(InputStream in, Principal principal)
            throws SAXException, InternalBusinessException, InvalidFormatException {
        Document document = null;
        try {
            // parse the input into a DOM
            document = parseInput(in, null);

            // validate incoming document against its schema
            if (isEPCISDocument(document)) {
                validateDocument(document, getSchema());
            } else if (isEPCISMasterDataDocument(document)) {
                validateDocument(document, getMasterDataSchema());
            }
        } catch (IOException e) {
            throw new InternalBusinessException("unable to read from input: " + e.getMessage(), e);
        }

        // start the capture operation
        Session session = null;
        try {
            session = sessionFactory.openSession();
            Transaction tx = null;
            try {
                tx = session.beginTransaction();
                LOG.debug("DB connection opened.");
                if (isEPCISDocument(document)) {
                    processEvents(session, document);
                } else if (isEPCISMasterDataDocument(document)) {
                    processMasterData(session, document);
                }
                tx.commit();
                // return OK
                LOG.info("EPCIS Capture Interface request succeeded");
            } catch (SAXException e) {
                LOG.error("EPCIS Capture Interface request failed: " + e.toString());
                if (tx != null) {
                    tx.rollback();
                }
                throw e;
            } catch (InvalidFormatException e) {
                LOG.error("EPCIS Capture Interface request failed: " + e.toString());
                if (tx != null) {
                    tx.rollback();
                }
                throw e;
            } catch (Exception e) {
                // Hibernate throws RuntimeExceptions, so don't let them
                // (or anything else) escape without clean up
                LOG.error("EPCIS Capture Interface request failed: " + e.toString(), e);
                if (tx != null) {
                    tx.rollback();
                }
                throw new InternalBusinessException(e.toString());
            }
        } finally {
            if (session != null) {
                session.close();
            }
            // sessionFactory.getStatistics().logSummary();
            LOG.debug("DB connection closed");
        }
    }

    /**
     * Validates the given document against the given schema.
     */
    private void validateDocument(Document document, Schema schema) throws SAXException, IOException {
        if (schema != null) {
            Validator validator = schema.newValidator();
            validator.validate(new DOMSource(document));
            LOG.info("Incoming capture request was successfully validated against the EPCISDocument schema");
        } else {
            LOG.warn("Schema validator unavailable. Unable to validate EPCIS capture event against schema!");
        }
    }

    /**
     * Parses the input into a DOM. If a schema is given, the input is also
     * validated against this schema. The schema may be null.
     */
    private Document parseInput(InputStream in, Schema schema)
            throws InternalBusinessException, SAXException, IOException {
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        factory.setNamespaceAware(true);
        factory.setIgnoringComments(true);
        factory.setIgnoringElementContentWhitespace(true);
        factory.setSchema(schema);
        try {
            DocumentBuilder builder = factory.newDocumentBuilder();
            builder.setErrorHandler(new ErrorHandler() {
                public void warning(SAXParseException e) throws SAXException {
                    LOG.warn("warning while parsing XML input: " + e.getMessage());
                }

                public void fatalError(SAXParseException e) throws SAXException {
                    LOG.error("non-recovarable error while parsing XML input: " + e.getMessage());
                    throw e;
                }

                public void error(SAXParseException e) throws SAXException {
                    LOG.error("error while parsing XML input: " + e.getMessage());
                    throw e;
                }
            });
            Document document = builder.parse(in);
            LOG.debug("payload successfully parsed as XML document");
            if (LOG.isDebugEnabled()) {
                logDocument(document);
            }
            return document;
        } catch (ParserConfigurationException e) {
            throw new InternalBusinessException("unable to configure document builder to parse XML input", e);
        }
    }

    /**
     * prints the given Document to the log file if it does not exceed a
     * specified size.
     */
    private void logDocument(Document document) {
        try {
            TransformerFactory tfFactory = TransformerFactory.newInstance();
            Transformer transformer = tfFactory.newTransformer();
            StringWriter writer = new StringWriter();
            transformer.transform(new DOMSource(document), new StreamResult(writer));
            String xml = writer.toString();
            if (xml.length() > 100 * 1024) {
                // too large, do not log
                xml = null;
            } else {
                LOG.debug("Incoming contents:\n\n" + writer.toString() + "\n");
            }
        } catch (Throwable t) {
            // never mind ... do not log
        }
    }

    /**
     * @return <code>true</code> if the given Document is an
     *         <i>EPCISDocument</i>.
     */
    private boolean isEPCISDocument(Document document) {
        return document.getDocumentElement().getLocalName().equals("EPCISDocument");
    }

    /**
     * @return <code>true</code> if the given Document is an
     *         <i>EPCISMasterDataDocument</i>.
     */
    private boolean isEPCISMasterDataDocument(Document document) {
        return document.getDocumentElement().getLocalName().equals("EPCISMasterDataDocument");
    }

    /**
     * Processes the given document and stores the events to db.
     */
    private void processEvents(Session session, Document document)
            throws DOMException, SAXException, InvalidFormatException {
        NodeList eventList = document.getElementsByTagName("EventList");
        NodeList events = eventList.item(0).getChildNodes();

        // walk through all supplied events
        int eventCount = 0;
        for (int i = 0; i < events.getLength(); i++) {
            Node eventNode = events.item(i);
            String nodeName = eventNode.getNodeName();

            if (nodeName.equals(EpcisConstants.OBJECT_EVENT) || nodeName.equals(EpcisConstants.AGGREGATION_EVENT)
                    || nodeName.equals(EpcisConstants.QUANTITY_EVENT)
                    || nodeName.equals(EpcisConstants.TRANSACTION_EVENT)) {
                LOG.debug("processing event " + i + ": '" + nodeName + "'.");
                handleEvent(session, eventNode, nodeName);
                eventCount++;
                if (eventCount % 50 == 0) {
                    session.flush();
                    session.clear();
                }
            } else if (!nodeName.equals("#text") && !nodeName.equals("#comment")) {
                throw new SAXException("Encountered unknown event '" + nodeName + "'.");
            }
        }
    }

    /**
     * Takes an XML document node, parses it as EPCIS event and inserts the data
     * into the database. The parse routine is generic for all event types; the
     * query generation part has some if/elses to take care of different event
     * parameters.
     * 
     * @param eventNode
     *            The current event node.
     * @param eventType
     *            The current event type.
     * @throws Exception
     * @throws DOMException
     */
    private void handleEvent(Session session, final Node eventNode, final String eventType)
            throws DOMException, SAXException, InvalidFormatException {
        if (eventNode == null) {
            // nothing to do
            return;
        } else if (eventNode.getChildNodes().getLength() == 0) {
            throw new SAXException("Event element '" + eventNode.getNodeName() + "' has no children elements.");
        }
        Node curEventNode = null;

        // A lot of the initialized variables have type URI. This type isn't to
        // compare with the URI-Type of the standard. In fact, most of the
        // variables having type URI are declared as Vocabularies in the
        // Standard. Commonly, we use String for the Standard-Type URI.

        Calendar eventTime = null;
        Calendar recordTime = GregorianCalendar.getInstance();
        String eventTimeZoneOffset = null;
        String action = null;
        String parentId = null;
        Long quantity = null;
        String bizStepUri = null;
        String dispositionUri = null;
        String readPointUri = null;
        String bizLocationUri = null;
        String epcClassUri = null;

        List<String> epcs = null;
        List<BusinessTransaction> bizTransList = null;
        List<EventFieldExtension> fieldNameExtList = new ArrayList<EventFieldExtension>();

        for (int i = 0; i < eventNode.getChildNodes().getLength(); i++) {
            curEventNode = eventNode.getChildNodes().item(i);
            String nodeName = curEventNode.getNodeName();

            if (nodeName.equals("#text") || nodeName.equals("#comment")) {
                // ignore text or comments
                LOG.debug("  ignoring text or comment: '" + curEventNode.getTextContent().trim() + "'");
                continue;
            }

            LOG.debug("  handling event field: '" + nodeName + "'");
            if (nodeName.equals("eventTime")) {
                String xmlTime = curEventNode.getTextContent();
                LOG.debug("    eventTime in xml is '" + xmlTime + "'");
                try {
                    eventTime = TimeParser.parseAsCalendar(xmlTime);
                } catch (ParseException e) {
                    throw new SAXException("Invalid date/time (must be ISO8601).", e);
                }
                LOG.debug("    eventTime parsed as '" + eventTime.getTime() + "'");
            } else if (nodeName.equals("recordTime")) {
                // ignore recordTime
            } else if (nodeName.equals("eventTimeZoneOffset")) {
                eventTimeZoneOffset = checkEventTimeZoneOffset(curEventNode.getTextContent());
            } else if (nodeName.equals("epcList") || nodeName.equals("childEPCs")) {
                epcs = handleEpcs(eventType, curEventNode);
            } else if (nodeName.equals("bizTransactionList")) {
                bizTransList = handleBizTransactions(session, curEventNode);
            } else if (nodeName.equals("action")) {
                action = curEventNode.getTextContent();
                if (!action.equals("ADD") && !action.equals("OBSERVE") && !action.equals("DELETE")) {
                    throw new SAXException("Encountered illegal 'action' value: " + action);
                }
            } else if (nodeName.equals("bizStep")) {
                bizStepUri = curEventNode.getTextContent();
            } else if (nodeName.equals("disposition")) {
                dispositionUri = curEventNode.getTextContent();
            } else if (nodeName.equals("readPoint")) {
                Element attrElem = (Element) curEventNode;
                Node id = attrElem.getElementsByTagName("id").item(0);
                readPointUri = id.getTextContent();
            } else if (nodeName.equals("bizLocation")) {
                Element attrElem = (Element) curEventNode;
                Node id = attrElem.getElementsByTagName("id").item(0);
                bizLocationUri = id.getTextContent();
            } else if (nodeName.equals("epcClass")) {
                epcClassUri = curEventNode.getTextContent();
            } else if (nodeName.equals("quantity")) {
                quantity = Long.valueOf(curEventNode.getTextContent());
            } else if (nodeName.equals("parentID")) {
                checkEpcOrUri(curEventNode.getTextContent(), false);
                parentId = curEventNode.getTextContent();
            } else {
                String[] parts = nodeName.split(":");
                if (parts.length == 2) {
                    LOG.debug("    treating unknown event field as extension.");
                    String prefix = parts[0];
                    String localname = parts[1];
                    // String namespace =
                    // document.getDocumentElement().getAttribute("xmlns:" +
                    // prefix);
                    String namespace = curEventNode.lookupNamespaceURI(prefix);
                    String value = curEventNode.getTextContent();
                    EventFieldExtension evf = new EventFieldExtension(prefix, namespace, localname, value);
                    fieldNameExtList.add(evf);
                } else {
                    // this is not a valid extension
                    throw new SAXException("    encountered unknown event field: '" + nodeName + "'.");
                }
            }
        }
        if (eventType.equals(EpcisConstants.AGGREGATION_EVENT)) {
            // for AggregationEvents, the parentID is only optional for
            // action=OBSERVE
            if (parentId == null && ("ADD".equals(action) || "DELETE".equals(action))) {
                throw new InvalidFormatException("'parentID' is required if 'action' is ADD or DELETE");
            }
        }

        // Changed by nkef (use "getOrEditVocabularyElement" instead of
        // "getOrInsertVocabularyElement")
        String nodeName = eventNode.getNodeName();
        VocabularyElement bizStep = bizStepUri != null
                ? getOrEditVocabularyElement(session, EpcisConstants.BUSINESS_STEP_ID, String.valueOf(bizStepUri),
                        "1")
                : null;
        VocabularyElement disposition = dispositionUri != null
                ? getOrEditVocabularyElement(session, EpcisConstants.DISPOSITION_ID, String.valueOf(dispositionUri),
                        "1")
                : null;
        VocabularyElement readPoint = readPointUri != null
                ? getOrEditVocabularyElement(session, EpcisConstants.READ_POINT_ID, String.valueOf(readPointUri),
                        "1")
                : null;
        VocabularyElement bizLocation = bizLocationUri != null
                ? getOrEditVocabularyElement(session, EpcisConstants.BUSINESS_LOCATION_ID,
                        String.valueOf(bizLocationUri), "1")
                : null;
        VocabularyElement epcClass = epcClassUri != null
                ? getOrEditVocabularyElement(session, EpcisConstants.EPC_CLASS_ID, String.valueOf(epcClassUri), "1")
                : null;

        BaseEvent be;
        if (nodeName.equals(EpcisConstants.AGGREGATION_EVENT)) {
            AggregationEvent ae = new AggregationEvent();
            ae.setParentId(parentId);
            ae.setChildEpcs(epcs);
            ae.setAction(Action.valueOf(action));
            be = ae;
        } else if (nodeName.equals(EpcisConstants.OBJECT_EVENT)) {
            ObjectEvent oe = new ObjectEvent();
            oe.setAction(Action.valueOf(action));
            if (epcs != null && epcs.size() > 0) {
                oe.setEpcList(epcs);
            }
            be = oe;
        } else if (nodeName.equals(EpcisConstants.QUANTITY_EVENT)) {
            QuantityEvent qe = new QuantityEvent();
            qe.setEpcClass((EPCClass) epcClass);
            if (quantity != null) {
                qe.setQuantity(quantity.longValue());
            }
            be = qe;
        } else if (nodeName.equals(EpcisConstants.TRANSACTION_EVENT)) {
            TransactionEvent te = new TransactionEvent();
            te.setParentId(parentId);
            te.setEpcList(epcs);
            te.setAction(Action.valueOf(action));
            be = te;
        } else {
            throw new SAXException("Encountered unknown event element '" + nodeName + "'.");
        }

        be.setEventTime(eventTime);
        be.setRecordTime(recordTime);
        be.setEventTimeZoneOffset(eventTimeZoneOffset);
        be.setBizStep((BusinessStepId) bizStep);
        be.setDisposition((DispositionId) disposition);
        be.setBizLocation((BusinessLocationId) bizLocation);
        be.setReadPoint((ReadPointId) readPoint);
        if (bizTransList != null && bizTransList.size() > 0) {
            be.setBizTransList(bizTransList);
        }
        if (!fieldNameExtList.isEmpty()) {
            be.setExtensions(fieldNameExtList);
        }

        session.save(be);
    }

    /**
     * Processes the given document and stores the masterdata to db.
     */
    private void processMasterData(Session session, Document document)
            throws DOMException, SAXException, InvalidFormatException {

        // Handle Vocabulary List
        NodeList vocabularyList = document.getElementsByTagName("VocabularyList");
        if (vocabularyList.item(0).hasChildNodes()) {
            NodeList vocabularys = vocabularyList.item(0).getChildNodes();

            // walk through all supplied vocabularies
            int vocabularyCount = 0;
            for (int i = 0; i < vocabularys.getLength(); i++) {
                Node vocabularyNode = vocabularys.item(i);
                String nodeName = vocabularyNode.getNodeName();
                if (nodeName.equals("Vocabulary")) {

                    String vocabularyType = vocabularyNode.getAttributes().getNamedItem("type").getNodeValue();

                    if (EpcisConstants.VOCABULARY_TYPES.contains(vocabularyType)) {

                        LOG.debug("processing " + i + ": '" + nodeName + "':" + vocabularyType + ".");
                        handleVocabulary(session, vocabularyNode, vocabularyType);
                        vocabularyCount++;
                        if (vocabularyCount % 50 == 0) {
                            session.flush();
                            session.clear();
                        }
                    }
                } else if (!nodeName.equals("#text") && !nodeName.equals("#comment")) {
                    throw new SAXException("Encountered unknown vocabulary '" + nodeName + "'.");
                }
            }
        }

    }

    /**
     * (nkef) Takes an XML document node, parses it as EPCIS Master Data and
     * inserts the data into the database. The parse routine is generic for all
     * Vocabulary types;
     * 
     * @param vocNode
     *            The current vocabulary node.
     * @param vocType
     *            The current vocabulary type.
     * @throws Exception
     * @throws DOMException
     */
    private void handleVocabulary(Session session, final Node vocNode, final String vocType)
            throws DOMException, SAXException, InvalidFormatException {
        if (vocNode == null) {
            // nothing to do
            return;
        } else if (vocNode.getChildNodes().getLength() == 0) {
            throw new SAXException("Vocabulary element '" + vocNode.getNodeName() + "' has no children elements.");
        }

        for (int i = 0; i < vocNode.getChildNodes().getLength(); i++) {
            Node curVocNode = vocNode.getChildNodes().item(i);
            if (isTextOrComment(curVocNode)) {
                continue;
            }
            for (int j = 0; j < curVocNode.getChildNodes().getLength(); j++) {
                Node curVocElemNode = curVocNode.getChildNodes().item(j);
                if (isTextOrComment(curVocElemNode)) {
                    continue;
                }
                LOG.debug("  processing vocabulary '" + curVocElemNode.getNodeName() + "'");
                String curVocElemId = curVocElemNode.getAttributes().getNamedItem("id").getNodeValue();
                /*
                 * vocabularyElementEditMode 1: insert((it can be anything
                 * except 2,3,4)) 2: alterURI 3: singleDelete 4: Delete element
                 * with it's direct or indirect descendants
                 */
                String vocElemEditMode = "";

                if (!(curVocElemNode.getAttributes().getNamedItem("mode") == null)) {
                    vocElemEditMode = curVocElemNode.getAttributes().getNamedItem("mode").getNodeValue();
                } else {
                    vocElemEditMode = "1";
                }

                VocabularyElement curVocElem = getOrEditVocabularyElement(session, vocType, curVocElemId,
                        vocElemEditMode);

                // *****************************************
                if (curVocElem != null) {
                    for (int k = 0; k < curVocElemNode.getChildNodes().getLength(); k++) {
                        Node curVocAttrNode = curVocElemNode.getChildNodes().item(k);
                        if (isTextOrComment(curVocAttrNode)) {
                            continue;
                        }

                        LOG.debug("  processing vocabulary attribute '" + curVocAttrNode.getNodeName() + "'");
                        String curVocAttrId = curVocAttrNode.getAttributes().getNamedItem("id").getNodeValue();
                        String curVocAttrValue = parseVocAttributeValue(curVocAttrNode);

                        /*
                         * vocabularyAttributeEditMode 1: Insert (it can be
                         * anything except 3)) 2: Alter Attribute Value (it can
                         * be anything except 3) 3: Delete Attribute (required)
                         */
                        String vocabularyAttributeEditMode = "";
                        if (!(curVocAttrNode.getAttributes().getNamedItem("mode") == null)) {
                            vocabularyAttributeEditMode = curVocAttrNode.getAttributes().getNamedItem("mode")
                                    .getNodeValue();
                        } else {
                            vocabularyAttributeEditMode = "add/alter";
                        }

                        getOrEditVocabularyAttributeElement(session, vocType, curVocElem.getId(), curVocAttrId,
                                curVocAttrValue, vocabularyAttributeEditMode);
                    }
                }
                // *****************************************
            }
        }
    }

    private boolean isTextOrComment(Node node) {
        return node.getNodeType() == Node.TEXT_NODE || node.getNodeType() == Node.COMMENT_NODE;
    }

    /**
     * Parses the attribute <b>value</b> of a VocabularyElement. The value can
     * be null in which case an empty String is returned. Otherwise, the value
     * is either given as XML attribute named 'value' or as inline text, see the
     * sample below. <br>
     * 
     * <pre>
     * {@code
     * <VocabularyElement id="urn:epc:id:sgln:0037000.00729.0">
     *   <attribute id="urn:epcglobal:fmcg:mda:slt:retail"/>
     *   <attribute id="urn:epcglobal:fmcg:mda:location">Warehouse 1</attribute>
     *   <attribute id="urn:epcglobal:fmcg:mda:room" value="22b"/>
     *   <attribute id="urn:epcglobal:fmcg:mda:address">
     *     <sample:Address xmlns:sample="http://sample.com/ComplexTypeExample">
     *       <Street>100 Nowhere Street</Street>
     *       <City>Fancy</City>
     *     </sample:Address>
     *   </attribute>
     * </VocabularyElement>
     * }
     * </pre>
     * 
     * @return the attribute value as String.
     */
    private String parseVocAttributeValue(Node vocAttrNode) {
        String vocAttrValue;
        if (vocAttrNode.getAttributes().getNamedItem("value") != null) {
            // the value is given as attribute 'value'
            vocAttrValue = vocAttrNode.getAttributes().getNamedItem("value").getNodeValue();
        } else if (vocAttrNode.getChildNodes().getLength() > 1) {
            // the value is given as DOM-tree
            TransformerFactory transFactory = TransformerFactory.newInstance();
            StringWriter buffer = new StringWriter();
            try {
                Transformer transformer = transFactory.newTransformer();
                transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
                transformer.setOutputProperty(OutputKeys.INDENT, "no");
                transformer.transform(new DOMSource(vocAttrNode.getChildNodes().item(1)), new StreamResult(buffer));
                vocAttrValue = buffer.toString();
            } catch (Throwable t) {
                LOG.warn("unable to transform vocabulary attribute value (XML) into a string", t);
                vocAttrValue = vocAttrNode.getTextContent();
            }
        } else if (vocAttrNode.getFirstChild() != null) {
            // the value is given as text
            vocAttrValue = vocAttrNode.getFirstChild().getNodeValue();
        } else {
            vocAttrValue = "";
        }
        return vocAttrValue;
    }

    /**
     * Parses the xml tree for epc nodes and returns a list of EPC URIs.
     * 
     * @param eventType
     * @param epcNode
     *            The parent Node from which EPC URIs should be extracted.
     * @return An array of vocabularies containing all the URIs found in the
     *         given node.
     * @throws SAXException
     *             If an unknown tag (no &lt;epc&gt;) is encountered.
     * @throws InvalidFormatException
     * @throws DOMException
     */
    private List<String> handleEpcs(final String eventType, final Node epcNode)
            throws SAXException, DOMException, InvalidFormatException {
        List<String> epcList = new ArrayList<String>();

        boolean isEpc = false;
        boolean epcRequired = false;
        boolean atLeastOneNonEpc = false;
        for (int i = 0; i < epcNode.getChildNodes().getLength(); i++) {
            Node curNode = epcNode.getChildNodes().item(i);
            if (curNode.getNodeName().equals("epc")) {
                isEpc = checkEpcOrUri(curNode.getTextContent(), epcRequired);
                if (isEpc) {
                    // if one of the values is an EPC, then all of them must be
                    // valid EPCs
                    epcRequired = true;
                } else {
                    atLeastOneNonEpc = true;
                }
                epcList.add(curNode.getTextContent());
            } else {
                if (curNode.getNodeName() != "#text" && curNode.getNodeName() != "#comment") {
                    throw new SAXException("Unknown XML tag: " + curNode.getNodeName(), null);
                }
            }
        }
        if (atLeastOneNonEpc && isEpc) {
            throw new InvalidFormatException(
                    "One of the provided EPCs was a 'pure identity' EPC, so all of them must be 'pure identity' EPCs");
        }
        return epcList;
    }

    /**
     * @param epcOrUri
     *            The EPC or URI to check.
     * @param epcRequired
     *            <code>true</code> if an EPC is required (will throw an
     *            InvalidFormatException if the given <code>epcOrUri</code> is
     *            an invalid EPC, but might be a valid URI), <code>false</code>
     *            otherwise.
     * @return <code>true</code> if the given <code>epcOrUri</code> is a valid
     *         EPC, <code>false</code> otherwise.
     * @throws InvalidFormatException
     */
    protected boolean checkEpcOrUri(String epcOrUri, boolean epcRequired) throws InvalidFormatException {
        boolean isEpc = false;
        if (epcOrUri.startsWith("urn:epc:id:")) {
            // check if it is a valid EPC
            checkEpc(epcOrUri);
            isEpc = true;
        } else {
            // childEPCs in AggregationEvents, and epcList in
            // TransactionEvents might also be simple URIs
            if (epcRequired) {
                throw new InvalidFormatException(
                        "One of the provided EPCs was a 'pure identity' EPC, so all of them must be 'pure identity' EPCs");
            }
            checkUri(epcOrUri);
        }
        return isEpc;
    }

    /**
     * Parses the xml tree for epc nodes and returns a List of BizTransaction
     * URIs with their corresponding type.
     * 
     * @param bizNode
     *            The parent Node from which BizTransaction URIs should be
     *            extracted.
     * @return A List of BizTransaction.
     * @throws SAXException
     *             If an unknown tag (no &lt;epc&gt;) is encountered.
     */
    private List<BusinessTransaction> handleBizTransactions(Session session, Node bizNode) throws SAXException {
        List<BusinessTransaction> bizTransactionList = new ArrayList<BusinessTransaction>();

        for (int i = 0; i < bizNode.getChildNodes().getLength(); i++) {
            Node curNode = bizNode.getChildNodes().item(i);
            if (curNode.getNodeName().equals("bizTransaction")) {

                // Changed by nkef (use "getOrEditVocabularyElement" instead of
                // "getOrInsertVocabularyElement")
                String bizTransTypeUri = curNode.getAttributes().item(0).getTextContent();
                String bizTransUri = curNode.getTextContent();
                BusinessTransactionId bizTrans = (BusinessTransactionId) getOrEditVocabularyElement(session,
                        EpcisConstants.BUSINESS_TRANSACTION_ID, bizTransUri.toString(), "1");
                BusinessTransactionTypeId type = (BusinessTransactionTypeId) getOrEditVocabularyElement(session,
                        EpcisConstants.BUSINESS_TRANSACTION_TYPE_ID, bizTransTypeUri.toString(), "1");

                Criteria c0 = session.createCriteria(BusinessTransaction.class);
                c0.add(Restrictions.eq("bizTransaction", bizTrans));
                c0.add(Restrictions.eq("type", type));
                BusinessTransaction bizTransaction = (BusinessTransaction) c0.uniqueResult();

                if (bizTransaction == null) {
                    bizTransaction = new BusinessTransaction();
                    bizTransaction.setBizTransaction(bizTrans);
                    bizTransaction.setType(type);
                    session.save(bizTransaction);
                }

                bizTransactionList.add(bizTransaction);

            } else {
                if (!curNode.getNodeName().equals("#text") && !curNode.getNodeName().equals("#comment")) {
                    throw new SAXException("Unknown XML tag: " + curNode.getNodeName(), null);
                }
            }
        }
        return bizTransactionList;
    }

    /**
     * (depricated) Inserts vocabulary into the database by searching for
     * already existing entries; if found, the corresponding ID is returned. If
     * not found, the vocabulary is extended if "insertmissingvoc" is true;
     * otherwise an SQLException is thrown
     * 
     * @param tableName
     *            The name of the vocabulary table.
     * @param uri
     *            The vocabulary adapting the URI to be inserted into the
     *            vocabulary table.
     * @return The ID of an already existing vocabulary table with the given
     *         uri.
     * @throws UnsupportedOperationException
     *             If we are not allowed to insert a missing vocabulary.
     */
    public VocabularyElement getOrInsertVocabularyElement(Session session, String vocabularyType,
            String vocabularyElement) throws SAXException {
        Class<?> c = vocClassMap.get(vocabularyType);
        Criteria c0 = session.createCriteria(c);
        c0.setCacheable(true);
        c0.add(Restrictions.eq("uri", vocabularyElement));
        VocabularyElement ve;
        try {
            ve = (VocabularyElement) c0.uniqueResult();
        } catch (ObjectNotFoundException e) {
            ve = null;
        }
        if (ve == null) {
            // the uri does not yet exist: insert it if allowed. According to
            // the specs, some vocabulary is not allowed to be extended; this is
            // currently ignored here
            if (!insertMissingVoc) {
                throw new UnsupportedOperationException(
                        "Not allowed to add new vocabulary - use existing vocabulary");
            } else {
                // VocabularyElement subclasses should always have public
                // zero-arg constructor to avoid problems here
                try {
                    ve = (VocabularyElement) c.newInstance();
                } catch (InstantiationException e) {
                    throw new RuntimeException(e);
                } catch (IllegalAccessException e) {
                    throw new RuntimeException(e);
                }

                ve.setUri(vocabularyElement);
                session.save(ve);
                session.flush();
            }
        }
        return ve;
    }

    /**
     * (changed by nkef to support MasterDataCapture. Previusly known as
     * "getOrInsertVocabularyElement") Inserts vocabulary into the database by
     * searching for already existing entries; if found, the corresponding ID is
     * returned. If not found, the vocabulary is extended if "insertmissingvoc"
     * is true; otherwise an SQLException is thrown
     * 
     * @param tableName
     *            The name of the vocabulary table.
     * @param uri
     *            The vocabulary adapting the URI to be inserted into the
     *            vocabulary table.
     * @return The ID of an already existing vocabulary table with the given
     *         uri.
     * @throws UnsupportedOperationException
     *             If we are not allowed to insert a missing vocabulary.
     */
    public VocabularyElement getOrEditVocabularyElement(Session session, String vocabularyType,
            String vocabularyElementURI, String mode) throws SAXException {
        boolean alterURI = false;
        boolean singleDelete = false;
        boolean wdDelete = false;
        Long vocabularyElementID = null;

        if (mode.equals("2")) {
            alterURI = true;
        } else if (mode.equals("3")) {
            singleDelete = true;
        } else if (mode.equals("4")) {
            wdDelete = true;
        }

        Class<?> c = vocClassMap.get(vocabularyType);
        Criteria c0 = session.createCriteria(c);
        c0.setCacheable(true);
        c0.add(Restrictions.eq("uri", alterURI ? vocabularyElementURI.split("#")[0] : vocabularyElementURI));
        VocabularyElement ve;
        try {
            ve = (VocabularyElement) c0.uniqueResult();
        } catch (ObjectNotFoundException e) {
            ve = null;
        }
        if (ve != null) {
            vocabularyElementID = ve.getId();
        }

        if (ve == null || ((singleDelete || alterURI || wdDelete) && ve != null)) {
            // the uri does not yet exist: insert it if allowed. According to
            // the specs, some vocabulary is not allowed to be extended; this is
            // currently ignored here
            if (!insertMissingVoc) {
                throw new UnsupportedOperationException(
                        "Not allowed to add new vocabulary - use existing vocabulary");
            } else {
                // VocabularyElement subclasses should always have public
                // zero-arg constructor to avoid problems here

                if (alterURI) {
                    ve.setUri(vocabularyElementURI.split("#")[1]);
                    session.update(ve);
                    session.flush();
                    return ve;

                } else if (singleDelete) {
                    Object vocabularyElementObject = session.get(c, vocabularyElementID);
                    if (vocabularyElementObject != null)
                        session.delete(vocabularyElementObject);
                    deleteVocabularyElementAttributes(session, vocabularyType, vocabularyElementID);
                    session.flush();
                    return null;
                } else if (wdDelete) {
                    Object vocabularyElementObject = session.get(c, vocabularyElementID);
                    if (vocabularyElementObject != null)
                        session.delete(vocabularyElementObject);
                    deleteVocabularyElementAttributes(session, vocabularyType, vocabularyElementID);
                    deleteVocabularyElementDescendants(session, vocabularyType, vocabularyElementURI);
                    session.flush();
                    return null;

                } else {

                    try {
                        ve = (VocabularyElement) c.newInstance();
                    } catch (InstantiationException e) {
                        throw new RuntimeException(e);
                    } catch (IllegalAccessException e) {
                        throw new RuntimeException(e);
                    }

                    ve.setUri(vocabularyElementURI);
                    session.save(ve);
                }

                session.flush();
            }
        }
        return ve;
    }

    /**
     * (nkef) Delete the a vocabulary's Element Descendants and all of their
     * Attributes
     * 
     * @param session
     * @param vocabularyType
     * @param vocabularyElementURI
     */
    private void deleteVocabularyElementDescendants(Session session, String vocabularyType,
            String vocabularyElementURI) {
        Class<?> c = vocClassMap.get(vocabularyType);
        List<?> vocElementChildrens = session.createSQLQuery("SELECT * FROM "
                + vocabularyTablenameMap.get(vocabularyType) + " WHERE uri LIKE '" + vocabularyElementURI + ",%'")
                .addEntity(c).list();
        for (int i = 0; i < vocElementChildrens.size(); i++) {
            session.delete((VocabularyElement) vocElementChildrens.get(i));
            deleteVocabularyElementAttributes(session, vocabularyType,
                    ((VocabularyElement) vocElementChildrens.get(i)).getId());
        }
        session.flush();
    }

    /**
     * (nkef) Delete selected id vocabulary elements attributes
     * 
     * @param session
     * @param vocabularyType
     * @param vocabularyElementID
     */
    private void deleteVocabularyElementAttributes(Session session, String vocabularyType,
            Long vocabularyElementID) {
        Class<?> c = vocAttributeClassMap.get(vocabularyType);
        List<?> vocAttributeElements = session.createSQLQuery("select * FROM "
                + vocAttributeTablesMap.get(vocabularyType) + " where id = '" + vocabularyElementID + "'")
                .addEntity(c).list();
        for (int i = 0; i < vocAttributeElements.size(); i++) {
            session.delete((VocabularyAttributeElement) vocAttributeElements.get(i));
        }
        session.flush();

    }

    /**
     * (nkef) Inserts vocabulary attribute into the database by searching for
     * already existing entries; if found, the corresponding ID is returned. If
     * not found, the vocabulary is extended if "insertmissingvoc" is true;
     * otherwise an SQLException is thrown
     * 
     * @param tableName
     *            The name of the vocabulary table.
     * @param uri
     *            The vocabulary adapting the URI to be inserted into the
     *            vocabulary table.
     * @return The ID of an already existing vocabulary table with the given
     *         uri.
     * @throws UnsupportedOperationException
     *             If we are not allowed to insert a missing vocabulary.
     */
    public VocabularyAttributeElement getOrEditVocabularyAttributeElement(Session session, String vocabularyType,
            Long vocabularyElementID, String vocabularyAttributeElement, String vocabularyAttributeElementValue,
            String mode) throws SAXException {

        boolean deleteAttribute = false;

        if (mode.equals("3")) {
            deleteAttribute = true;
        }
        Class<?> c = vocAttributeClassMap.get(vocabularyType);
        Criteria c0 = session.createCriteria(c);
        c0.setCacheable(true);

        VocabularyAttrCiD vocabularyAttrCiD = new VocabularyAttrCiD();
        vocabularyAttrCiD.setAttribute(vocabularyAttributeElement);
        vocabularyAttrCiD.setId(vocabularyElementID);

        c0.add(Restrictions.idEq(vocabularyAttrCiD));

        VocabularyAttributeElement vocAttributeElement = null;

        try {
            vocAttributeElement = (VocabularyAttributeElement) c0.uniqueResult();
        } catch (ObjectNotFoundException e) {
            vocAttributeElement = null;
            e.printStackTrace();
        }

        if (vocAttributeElement == null || (deleteAttribute && (vocAttributeElement != null))
                || vocAttributeElement != null) {
            // the uri does not yet exist: insert it if allowed. According to
            // the specs, some vocabulary is not allowed to be extended; this is
            // currently ignored here
            if (!insertMissingVoc) {
                throw new UnsupportedOperationException(
                        "Not allowed to add new vocabulary - use existing vocabulary");
            } else {
                // VocabularyAttributeElement subclasses should always have
                // public zero-arg constructor to avoid problems here
                try {
                    vocAttributeElement = (VocabularyAttributeElement) c.newInstance();
                } catch (InstantiationException e) {
                    throw new RuntimeException(e);
                } catch (IllegalAccessException e) {
                    throw new RuntimeException(e);
                }
                vocAttributeElement.setVocabularyAttrCiD(vocabularyAttrCiD);
                vocAttributeElement.setValue(vocabularyAttributeElementValue);

                if (vocAttributeElement == null) {
                    session.save(vocAttributeElement);
                }

                else if (deleteAttribute) {
                    Object vocabularyAttr = session.get(c, vocabularyAttrCiD);
                    if (vocabularyAttr != null)
                        session.delete(vocabularyAttr);
                    session.flush();
                    return null;
                } else {
                    session.merge(vocAttributeElement);
                }

                session.flush();
            }
        }

        return vocAttributeElement;
    }

    /**
     * TODO: javadoc!
     * 
     * @param textContent
     * @return
     * @throws InvalidFormatException
     */
    protected String checkEventTimeZoneOffset(String textContent) throws InvalidFormatException {
        // first check the provided String against the expected pattern
        Pattern p = Pattern.compile("[+-]\\d\\d:\\d\\d");
        Matcher m = p.matcher(textContent);
        boolean matches = m.matches();
        if (matches) {
            // second check the values (hours and minutes)
            int h = Integer.parseInt(textContent.substring(1, 3));
            int min = Integer.parseInt(textContent.substring(4, 6));
            if ((h < 0) || (h > 14)) {
                matches = false;
            } else if (h == 14 && min != 0) {
                matches = false;
            } else if ((min < 0) || (min > 59)) {
                matches = false;
            }
        }
        if (matches) {
            return textContent;
        } else {
            throw new InvalidFormatException("'eventTimeZoneOffset' has invalid format: " + textContent);
        }
    }

    /**
     * TODO: javadoc!
     * 
     * @param textContent
     * @return
     * @throws InvalidFormatException
     */
    private boolean checkUri(String textContent) throws InvalidFormatException {
        try {
            new URI(textContent);
        } catch (URISyntaxException e) {
            throw new InvalidFormatException(e.getMessage(), e);
        }
        return true;
    }

    /**
     * Check EPC according to 'pure identity' URI as specified in Tag Data
     * Standard.
     * 
     * @param textContent
     * @throws InvalidFormatException
     */
    protected void checkEpc(String textContent) throws InvalidFormatException {
        String uri = textContent;
        if (!uri.startsWith("urn:epc:id:")) {
            throw new InvalidFormatException("Invalid 'pure identity' EPC format: must start with \"urn:epc:id:\"");
        }
        uri = uri.substring("urn:epc:id:".length());

        // check the patterns for the different EPC types
        String epcType = uri.substring(0, uri.indexOf(":"));
        uri = uri.substring(epcType.length() + 1);
        LOG.debug("Checking pattern for EPC type " + epcType + ": " + uri);
        Pattern p;
        if ("gid".equals(epcType)) {
            p = Pattern.compile("((0|[1-9][0-9]*)\\.){2}(0|[1-9][0-9]*)");
        } else if ("sgtin".equals(epcType) || "sgln".equals(epcType) || "grai".equals(epcType)) {
            p = Pattern.compile(
                    "([0-9]+\\.){2}([0-9]|[A-Z]|[a-z]|[\\!\\(\\)\\*\\+\\-',:;=_]|(%(([0-9]|[A-F])|[a-f]){2}))+");
        } else if ("sscc".equals(epcType)) {
            p = Pattern.compile("[0-9]+\\.[0-9]+");
        } else if ("giai".equals(epcType)) {
            p = Pattern.compile(
                    "[0-9]+\\.([0-9]|[A-Z]|[a-z]|[\\!\\(\\)\\*\\+\\-',:;=_]|(%(([0-9]|[A-F])|[a-f]){2}))+");
        } else {
            throw new InvalidFormatException("Invalid 'pure identity' EPC format: unknown EPC type: " + epcType);
        }
        Matcher m = p.matcher(uri);
        if (!m.matches()) {
            throw new InvalidFormatException("Invalid 'pure identity' EPC format: pattern \"" + uri
                    + "\" is invalid for EPC type \"" + epcType + "\" - check with Tag Data Standard");
        }

        // check the number of digits for the different EPC types
        boolean exceeded = false;
        int count1 = uri.indexOf(".");
        if ("sgtin".equals(epcType)) {
            int count2 = uri.indexOf(".", count1 + 1) - (count1 + 1);
            if (count1 + count2 > 13) {
                exceeded = true;
            }
        } else if ("sgln".equals(epcType)) {
            int count2 = uri.indexOf(".", count1 + 1) - (count1 + 1);
            if (count1 + count2 > 12) {
                exceeded = true;
            }
        } else if ("grai".equals(epcType)) {
            int count2 = uri.indexOf(".", count1 + 1) - (count1 + 1);
            if (count1 + count2 > 12) {
                exceeded = true;
            }
        } else if ("sscc".equals(epcType)) {
            int count2 = uri.length() - (count1 + 1);
            if (count1 + count2 > 17) {
                exceeded = true;
            }
        } else if ("giai".equals(epcType)) {
            int count2 = uri.length() - (count1 + 1);
            if (count1 + count2 > 30) {
                exceeded = true;
            }
        } else {
            // nothing to count
        }
        if (exceeded) {
            throw new InvalidFormatException(
                    "Invalid 'pure identity' EPC format: check allowed number of characters for EPC type '"
                            + epcType + "'");
        }
    }

    public SessionFactory getSessionFactory() {
        return sessionFactory;
    }

    public void setSessionFactory(SessionFactory sessionFactory) {
        this.sessionFactory = sessionFactory;
    }

    public boolean isDbResetAllowed() {
        return dbResetAllowed;
    }

    public void setDbResetAllowed(boolean dbResetAllowed) {
        this.dbResetAllowed = dbResetAllowed;
    }

    public void setDbResetScript(String dbResetScript) {
        if (dbResetScript != null) {
            String[] scripts = dbResetScript.split(",");
            List<File> scriptList = new ArrayList<File>(scripts.length);
            for (String script : scripts) {
                if (!StringUtils.isBlank(script)) {
                    script = "/" + script.trim();
                    URL url = ClassLoader.getSystemResource(script);
                    if (url == null) {
                        url = this.getClass().getResource(script);
                    }
                    if (url == null) {
                        LOG.warn("unable to find sql script " + script + " in classpath");
                    } else {
                        LOG.debug("found dbReset sql script at " + url);
                        scriptList.add(new File(url.getFile()));
                    }
                }
            }
            this.dbResetScripts = scriptList;
        }
    }

    public boolean isInsertMissingVoc() {
        return insertMissingVoc;
    }

    public void setInsertMissingVoc(boolean insertMissingVoc) {
        this.insertMissingVoc = insertMissingVoc;
    }

    public Schema getSchema() {
        return schema;
    }

    public void setSchema(Schema schema) {
        this.schema = schema;
    }

    public void setEpcisSchemaFile(String epcisSchemaFile) {
        Schema schema = initEpcisSchema(epcisSchemaFile);
        setSchema(schema);
    }

    public void setEpcisMasterdataSchemaFile(String epcisMasterdataSchemaFile) {
        Schema schema = initEpcisSchema(epcisMasterdataSchemaFile);
        setMasterDataSchema(schema);
    }

    public Schema getMasterDataSchema() {
        return masterDataSchema;
    }

    public void setMasterDataSchema(Schema masterDataSchema) {
        this.masterDataSchema = masterDataSchema;
    }
}