# 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);
        }
    }