CDataDocumentDecorator.java

package org.thewonderlemming.c4plantuml.graphml.export;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.Optional;

import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.thewonderlemming.c4plantuml.graphml.model.DataModel;
import org.thewonderlemming.c4plantuml.graphml.validation.GraphMLValidator;
import org.thewonderlemming.c4plantuml.graphml.validation.ValidationException;
import org.w3c.dom.Attr;
import org.w3c.dom.CDATASection;
import org.w3c.dom.Comment;
import org.w3c.dom.DOMConfiguration;
import org.w3c.dom.DOMImplementation;
import org.w3c.dom.Document;
import org.w3c.dom.DocumentFragment;
import org.w3c.dom.DocumentType;
import org.w3c.dom.Element;
import org.w3c.dom.EntityReference;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.ProcessingInstruction;
import org.w3c.dom.Text;
import org.w3c.dom.UserDataHandler;

/**
 * A {@link Document} decorator to handle CDATA tags around values in JAXB marshalling operations.
 *
 * @author thewonderlemming
 *
 */
public class CDataDocumentDecorator implements Document {

    private static final String INDENT_AMOUNT_OUTPUT_PROPERTY = "{http://xml.apache.org/xslt}indent-amount";

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

    private final Document document;


    /**
     * A factory method that builds a new {@link Document} and decorates it with the returned
     * {@link CDataDocumentDecorator} instance.
     *
     * @return a new {@link CDataDocumentDecorator} instance.
     * @throws ParserConfigurationException if anything goes wrong while creating the {@link Document} instance.
     */
    public static CDataDocumentDecorator newInstance() throws ParserConfigurationException {

        final DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance();
        docBuilderFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "");
        docBuilderFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "");

        final Document document = docBuilderFactory.newDocumentBuilder().newDocument();

        return CDataDocumentDecorator.newInstance(document);
    }

    /**
     * A factory method that builds a new {@link CDataDocumentDecorator} instance given a {@link Document} to decorate.
     *
     * @param decorated the {@link Document} to decorate.
     * @return a new {@link CDataDocumentDecorator} instance.
     */
    public static CDataDocumentDecorator newInstance(final Document decorated) {
        return new CDataDocumentDecorator(decorated);
    }

    private CDataDocumentDecorator(final Document document) {
        this.document = document;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Node adoptNode(final Node source) {
        return this.document.adoptNode(source);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Node appendChild(final Node newChild) {
        return this.document.appendChild(newChild);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Node cloneNode(final boolean deep) {
        return this.document.cloneNode(deep);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public short compareDocumentPosition(final Node other) {
        return this.document.compareDocumentPosition(other);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Attr createAttribute(final String name) {
        return this.document.createAttribute(name);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Attr createAttributeNS(final String namespaceURI, final String qualifiedName) {
        return this.document.createAttributeNS(namespaceURI, qualifiedName);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public CDATASection createCDATASection(final String data) {
        return this.document.createCDATASection(data);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Comment createComment(final String data) {
        return this.document.createComment(data);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public DocumentFragment createDocumentFragment() {
        return this.document.createDocumentFragment();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Element createElement(final String tagName) {
        return this.document.createElement(tagName);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Element createElementNS(final String namespaceURI, final String qualifiedName) {
        return this.document.createElementNS(namespaceURI, qualifiedName);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public EntityReference createEntityReference(final String name) {
        return this.document.createEntityReference(name);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public ProcessingInstruction createProcessingInstruction(final String target, final String data) {
        return this.document.createProcessingInstruction(target, data);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Text createTextNode(final String data) {
        return this.document.createTextNode(data);
    }

    /**
     * Exports the content of the document as a string that contains CDATA tags around values.
     *
     * @param charset the {@link Charset} to use for the XML output.
     * @param indent a flag that tells whether or not the XML output should fit on a single line.
     * @param validate a flag that telles whether or not the XML output should be validated against an XSD.
     * @param strictValidation a flag that tells whether or not validation exception should void the marshalling result.
     * @return an {@link Optional} of the result of the XML marshalling operation. Can be empty.
     */
    public Optional<String> exportAsString(final Charset charset, final boolean indent, final boolean validate,
        final boolean strictValidation) {

        try (final ByteArrayOutputStream baos = new ByteArrayOutputStream()) {

            final TransformerFactory transformerFactory = TransformerFactory.newInstance();
            transformerFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "");
            transformerFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_STYLESHEET, "");

            final Transformer nullTransformer = transformerFactory.newTransformer();
            nullTransformer.setOutputProperty(OutputKeys.INDENT, indent ? "yes" : "no");
            nullTransformer.setOutputProperty(INDENT_AMOUNT_OUTPUT_PROPERTY, indent ? "4" : "0");
            nullTransformer.setOutputProperty(OutputKeys.ENCODING, charset.name());
            nullTransformer.setOutputProperty(OutputKeys.STANDALONE, "yes");
            nullTransformer.setOutputProperty(OutputKeys.CDATA_SECTION_ELEMENTS, DataModel.TAG_NAME);
            nullTransformer.transform(new DOMSource(this.document), new StreamResult(baos));

            final String graphML = baos.toString(charset.name());
            LOGGER.debug("Current (yet to be validated) GraphML:\n{}", graphML);

            if (validate) {
                try (final ByteArrayInputStream bais = new ByteArrayInputStream(graphML.getBytes())) {
                    GraphMLValidator.validate(bais, strictValidation);
                }
            }

            return Optional.of(graphML);

        } catch (final TransformerException | ValidationException | IOException e) {
            LOGGER.error("Cannot process the current DOM document because of the following: {}", e.getMessage(), e);
        }

        return Optional.empty();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public NamedNodeMap getAttributes() {
        return this.document.getAttributes();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getBaseURI() {
        return this.document.getBaseURI();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public NodeList getChildNodes() {
        return this.document.getChildNodes();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public DocumentType getDoctype() {
        return this.document.getDoctype();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Element getDocumentElement() {
        return this.document.getDocumentElement();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getDocumentURI() {
        return this.document.getDocumentURI();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public DOMConfiguration getDomConfig() {
        return this.document.getDomConfig();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Element getElementById(final String elementId) {
        return this.document.getElementById(elementId);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public NodeList getElementsByTagName(final String tagname) {
        return this.document.getElementsByTagName(tagname);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public NodeList getElementsByTagNameNS(final String namespaceURI, final String localName) {
        return this.document.getElementsByTagNameNS(namespaceURI, localName);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Object getFeature(final String feature, final String version) {
        return this.document.getFeature(feature, version);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Node getFirstChild() {
        return this.document.getFirstChild();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public DOMImplementation getImplementation() {
        return this.document.getImplementation();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getInputEncoding() {
        return this.document.getInputEncoding();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Node getLastChild() {
        return this.document.getLastChild();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getLocalName() {
        return this.document.getLocalName();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getNamespaceURI() {
        return this.document.getNamespaceURI();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Node getNextSibling() {
        return this.document.getNextSibling();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getNodeName() {
        return this.document.getNodeName();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public short getNodeType() {
        return this.document.getNodeType();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getNodeValue() {
        return this.document.getNodeValue();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Document getOwnerDocument() {
        return this.document.getOwnerDocument();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Node getParentNode() {
        return this.document.getParentNode();
    }

    /**
    * {@inheritDoc}
    */
    @Override
    public String getPrefix() {
        return this.document.getPrefix();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Node getPreviousSibling() {
        return this.document.getPreviousSibling();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean getStrictErrorChecking() {
        return this.document.getStrictErrorChecking();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getTextContent() {
        return this.document.getTextContent();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Object getUserData(final String key) {
        return this.document.getUserData(key);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getXmlEncoding() {
        return this.document.getXmlEncoding();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean getXmlStandalone() {
        return this.document.getXmlStandalone();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getXmlVersion() {
        return this.document.getXmlVersion();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean hasAttributes() {
        return this.document.hasAttributes();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean hasChildNodes() {
        return this.document.hasChildNodes();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Node importNode(final Node importedNode, final boolean deep) {
        return this.document.importNode(importedNode, deep);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Node insertBefore(final Node newChild, final Node refChild) {
        return this.document.insertBefore(newChild, refChild);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isDefaultNamespace(final String namespaceURI) {
        return this.document.isDefaultNamespace(namespaceURI);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isEqualNode(final Node arg) {
        return this.document.isEqualNode(arg);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isSameNode(final Node other) {
        return this.document.isSameNode(other);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isSupported(final String feature, final String version) {
        return this.document.isSupported(feature, version);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String lookupNamespaceURI(final String prefix) {
        return this.document.lookupNamespaceURI(prefix);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String lookupPrefix(final String namespaceURI) {
        return this.document.lookupPrefix(namespaceURI);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void normalize() {
        this.document.normalize();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void normalizeDocument() {
        this.document.normalizeDocument();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Node removeChild(final Node oldChild) {
        return this.document.removeChild(oldChild);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Node renameNode(final Node n, final String namespaceURI, final String qualifiedName) {
        return this.document.renameNode(n, namespaceURI, qualifiedName);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Node replaceChild(final Node newChild, final Node oldChild) {
        return this.document.replaceChild(newChild, oldChild);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void setDocumentURI(final String documentURI) {
        this.document.setDocumentURI(documentURI);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void setNodeValue(final String nodeValue) {
        this.document.setNodeValue(nodeValue);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void setPrefix(final String prefix) {
        this.document.setPrefix(prefix);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void setStrictErrorChecking(final boolean strictErrorChecking) {
        this.document.setStrictErrorChecking(strictErrorChecking);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void setTextContent(final String textContent) {
        this.document.setTextContent(textContent);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Object setUserData(final String key, final Object data, final UserDataHandler handler) {
        return this.document.setUserData(key, data, handler);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void setXmlStandalone(final boolean xmlStandalone) {
        this.document.setXmlStandalone(xmlStandalone);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void setXmlVersion(final String xmlVersion) {
        this.document.setXmlVersion(xmlVersion);
    }
}