org.codice.ddf.transformer.xml.streaming.lib.SaxEventHandlerDelegate.java Source code

Java tutorial

Introduction

Here is the source code for org.codice.ddf.transformer.xml.streaming.lib.SaxEventHandlerDelegate.java

Source

/**
 * Copyright (c) Codice Foundation
 * <p>
 * This is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser
 * General Public License as published by the Free Software Foundation, either version 3 of the
 * License, or any later version.
 * <p>
 * This program 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. A copy of the GNU Lesser General Public License
 * is distributed along with this program and can be found at
 * <http://www.gnu.org/licenses/lgpl.html>.
 */
package org.codice.ddf.transformer.xml.streaming.lib;

import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import org.apache.commons.io.input.TeeInputStream;
import org.codice.ddf.transformer.xml.streaming.SaxEventHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xml.sax.Attributes;
import org.xml.sax.ErrorHandler;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.DefaultHandler;
import org.xml.sax.helpers.XMLReaderFactory;

import ddf.catalog.data.Attribute;
import ddf.catalog.data.Metacard;
import ddf.catalog.data.MetacardType;
import ddf.catalog.data.impl.AttributeImpl;
import ddf.catalog.data.impl.BasicTypes;
import ddf.catalog.data.impl.MetacardImpl;
import ddf.catalog.transform.CatalogTransformerException;

/**
 * This class is used by the {@link XmlInputTransformer} to delegate SAX Parse events to the relevant handlers.
 * Its primary method is {@link SaxEventHandlerDelegate#read} which takes in an {@link InputStream} returns a {@link Metacard},
 * populated with all the {@link Attribute}s parsed by the {@link SaxEventHandlerDelegate#eventHandlers}
 */
public class SaxEventHandlerDelegate extends DefaultHandler {

    private XMLReader parser;

    private List<SaxEventHandler> eventHandlers = new ArrayList<>();

    private static final Logger LOGGER = LoggerFactory.getLogger(SaxEventHandlerDelegate.class);

    private MetacardType metacardType = BasicTypes.BASIC_METACARD;

    private InputTransformerErrorHandler inputTransformerErrorHandler;

    public SaxEventHandlerDelegate() {
        try {
            parser = XMLReaderFactory.createXMLReader();
        } catch (Exception e) {
            LOGGER.debug(
                    "Exception thrown during creation of SaxEventHandlerDelegate. Probably caused by one of the setFeature calls",
                    e);
        }
    }

    public SaxEventHandlerDelegate(List<SaxEventHandler> eventHandlers) {
        this();
        this.eventHandlers = eventHandlers;
    }

    /**
     * Takes in an {@link InputStream} returns a {@link Metacard},
     * populated with all the {@link Attribute}s parsed by the {@link SaxEventHandlerDelegate#eventHandlers}
     *
     * @param inputStream an XML document that can be parsed into a Metacard
     * @return a {@link Metacard},
     * populated with all the {@link Attribute}s parsed by the {@link SaxEventHandlerDelegate#eventHandlers}
     * @throws CatalogTransformerException
     */
    public Metacard read(InputStream inputStream) throws CatalogTransformerException {

        /*
         * Create a new MetacardImpl with the proper MetacardType
         */
        Metacard metacard = new MetacardImpl(metacardType);

        try {
            InputSource newStream = new InputSource(new BufferedInputStream(inputStream));

            /*
             * Set the parser's ContentHandler to this delegate, which ensures the delegate receives all
             * parse events that should be handled by a SaxEventHandler (startElement, endElement, characters, startPrefixMapping, etc)
             * Set the parser's ErrorHandler to be a new InputTransformerHandler
             */
            parser.setContentHandler(this);
            InputTransformerErrorHandler inputTransformerErrorHandler = getInputTransformerErrorHandler()
                    .configure(new StringBuilder());
            parser.setErrorHandler(inputTransformerErrorHandler);
            parser.parse(newStream);

            /*
             * If any major errors occur during parsing, print them out and add them to the metacard as validation errors
             */
            List<Serializable> errorsAndWarnings = Arrays
                    .asList(inputTransformerErrorHandler.getParseWarningsErrors());
            if (!((String) errorsAndWarnings.get(0)).isEmpty()) {
                LOGGER.warn((String) errorsAndWarnings.get(0));
                Attribute attr;
                List<Serializable> values;
                if ((attr = metacard.getAttribute(BasicTypes.VALIDATION_ERRORS)) != null
                        && (values = attr.getValues()) != null) {
                    errorsAndWarnings.addAll(values);
                }
                metacard.setAttribute(new AttributeImpl(BasicTypes.VALIDATION_ERRORS, errorsAndWarnings));
            }

        } catch (IOException | SAXException e) {
            LOGGER.debug("Exception thrown during parsing of inputStream", e);
            throw new CatalogTransformerException("Could not properly parse metacard", e);
        }

        /*
         * Populate metacard with all attributes constructed in SaxEventHandlers during parsing
         */
        for (SaxEventHandler eventHandler : eventHandlers) {
            List<Attribute> attributes = eventHandler.getAttributes();
            for (Attribute attribute : attributes) {
                Attribute tmpAttr;

                /*
                 * If metacard already has values in the attribute, put them together into a multivalued list,
                 * instead of simply overwriting the existing values.
                 */
                if ((tmpAttr = metacard.getAttribute(attribute.getName())) != null) {
                    List<Serializable> tmpAttrValues = tmpAttr.getValues();

                    tmpAttrValues.addAll(attribute.getValues());
                    tmpAttr = new AttributeImpl(attribute.getName(), tmpAttrValues);
                    metacard.setAttribute(tmpAttr);

                } else {
                    metacard.setAttribute(attribute);
                }
            }
        }
        return metacard;
    }

    /**
     * Takes in a sax event from {@link SaxEventHandlerDelegate#parser}
     * and passes it to the {@link SaxEventHandlerDelegate#eventHandlers}
     */
    @Override
    public void startDocument() throws SAXException {
        for (SaxEventHandler transformer : eventHandlers) {
            transformer.startDocument();
        }
    }

    /**
     * Takes in a sax event from {@link SaxEventHandlerDelegate#parser}
     * and passes it to the {@link SaxEventHandlerDelegate#eventHandlers}
     *
     * @param uri        the URI that is passed in by {@link SaxEventHandlerDelegate}
     * @param localName  the localName that is passed in by {@link SaxEventHandlerDelegate}
     * @param qName      the qName that is passed in by {@link SaxEventHandlerDelegate}
     * @param attributes the attributes that are passed in by {@link SaxEventHandlerDelegate}
     * @throws SAXException
     */
    @Override
    public void startElement(String uri, String localName, String qName, Attributes attributes)
            throws SAXException {
        for (SaxEventHandler transformer : eventHandlers) {
            transformer.startElement(uri, localName, qName, attributes);
        }

    }

    /**
     * Takes in a sax event from {@link SaxEventHandlerDelegate#parser}
     * and passes it to the {@link SaxEventHandlerDelegate#eventHandlers}
     *
     * @param ch     the ch that is passed in by {@link SaxEventHandlerDelegate}
     * @param start  the start that is passed in by {@link SaxEventHandlerDelegate}
     * @param length the length that is passed in by {@link SaxEventHandlerDelegate}
     * @throws SAXException
     */
    @Override
    public void characters(char ch[], int start, int length) throws SAXException {
        for (SaxEventHandler transformer : eventHandlers) {
            transformer.characters(ch, start, length);
        }
    }

    /**
     * Takes in a sax event from {@link SaxEventHandlerDelegate#parser}
     * and passes it to the {@link SaxEventHandlerDelegate#eventHandlers}
     *
     * @param namespaceURI the namespaceURI that is passed in by {@link SaxEventHandlerDelegate}
     * @param localName    the localName that is passed in by {@link SaxEventHandlerDelegate}
     * @param qName        the qName that is passed in by {@link SaxEventHandlerDelegate}
     * @throws SAXException
     */
    @Override
    public void endElement(String namespaceURI, String localName, String qName) throws SAXException {

        for (SaxEventHandler transformer : eventHandlers) {
            transformer.endElement(namespaceURI, localName, qName);
        }

    }

    /**
     * Takes in a sax event from {@link SaxEventHandlerDelegate#parser}
     * and passes it to the {@link SaxEventHandlerDelegate#eventHandlers}
     *
     * @param prefix the prefix that is passed in by {@link SaxEventHandlerDelegate}
     * @param uri    the uri that is passed in by {@link SaxEventHandlerDelegate}
     * @throws SAXException
     */
    @Override
    public void startPrefixMapping(String prefix, String uri) throws SAXException {
        for (SaxEventHandler transformer : eventHandlers) {
            transformer.startPrefixMapping(prefix, uri);
        }
    }

    public SaxEventHandlerDelegate setMetacardType(MetacardType metacardType) {
        this.metacardType = metacardType;
        return this;
    }

    public TeeInputStream getMetadataStream(InputStream inputStream, OutputStream outputStream) {
        return new TeeInputStream(inputStream, outputStream);
    }

    InputTransformerErrorHandler getInputTransformerErrorHandler() {
        if (this.inputTransformerErrorHandler == null) {
            this.inputTransformerErrorHandler = new InputTransformerErrorHandler();
        }
        return this.inputTransformerErrorHandler;
    }
}

/**
 * A private class used to handle errors that occur during SAX Parsing. It allows all the errors
 * that occur during the parsing of a single document to be easily and succinctly logged at the end
 * of parsing.
 */
class InputTransformerErrorHandler implements ErrorHandler {

    private static final Logger LOGGER = LoggerFactory.getLogger(InputTransformerErrorHandler.class);

    private StringBuilder outWriter;

    /**
     * A helper method to help parse relevant information from the exception
     *
     * @param exception An exception that was passed into the {@link InputTransformerErrorHandler} by the parser
     * @return a string of relevant information about the exception
     */
    private String getParseExceptionInfo(SAXParseException exception) {
        String systemId = exception.getSystemId();

        if (systemId == null) {
            systemId = "null";
        }

        return "URI=" + systemId + " Line=" + exception.getLineNumber() + ": " + exception.getMessage();
    }

    /**
     * Takes in an warning exception thrown by the parser and writes relevant information about it to BufferedWriter
     *
     * @param exception an exception thrown by the parser
     * @throws SAXException
     */
    @Override
    public void warning(SAXParseException exception) throws SAXException {
        outWriter.append("Warning: " + getParseExceptionInfo(exception) + '\n');
    }

    /**
     * Takes in an error exception thrown by the parser and writes relevant information about it to BufferedWriter
     *
     * @param exception an exception thrown by the parser
     * @throws SAXException
     */
    @Override
    public void error(SAXParseException exception) throws SAXException {
        outWriter.append("Error: " + getParseExceptionInfo(exception) + '\n');
    }

    /**
     * Takes in a fatalError exception thrown by the parser and writes relevant information about it to BufferedWriter
     * Also, throws a new exception, because SAX parsing can not continue after a Fatal Error.
     *
     * @param exception an exception thrown by the parser
     * @throws SAXException
     */
    @Override
    public void fatalError(SAXParseException exception) throws SAXException {
        String message = "Fatal Error: " + getParseExceptionInfo(exception);
        outWriter.append(message + '\n');
        throw new SAXException(message);
    }

    /**
     * Gets the String value of the outWriter and resets the writer.
     *
     * @return a String containing a log of relevant information about warnings and errors that occured
     * during parsing
     */
    public String getParseWarningsErrors() {
        String returnString = outWriter.toString().trim();
        outWriter.setLength(0);
        return returnString;
    }

    public InputTransformerErrorHandler configure(StringBuilder outWriter) {
        this.outWriter = outWriter;
        return this;
    }
}