Xml.java

/*
 * Copyright (c) 2021 Mārtiņš Avots (Martins Avots) and others
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License 2.0, which is available at
 * http://www.eclipse.org/legal/epl-2.0, or the MIT License,
 * which is available at https://spdx.org/licenses/MIT.html.
 *
 * SPDX-License-Identifier: EPL-2.0 OR MIT
 */
package net.splitcells.dem.lang;

import net.splitcells.dem.data.set.list.Lists;
import net.splitcells.dem.lang.annotations.JavaLegacyArtifact;
import net.splitcells.dem.lang.namespace.NameSpace;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.*;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.StringWriter;
import java.nio.file.Path;
import java.util.Collection;
import java.util.Optional;
import java.util.stream.IntStream;
import java.util.stream.Stream;

import static java.util.Arrays.asList;
import static java.util.stream.Collectors.toList;
import static javax.xml.transform.OutputKeys.INDENT;
import static javax.xml.transform.OutputKeys.OMIT_XML_DECLARATION;
import static net.splitcells.dem.resource.communication.log.Domsole.domsole;
import static net.splitcells.dem.utils.ConstructorIllegal.constructorIllegal;
import static org.w3c.dom.Node.ELEMENT_NODE;

/**
 * Currently XML is used as the base of all documents.
 * If it is not suitable anymore, it will be replaced by {@link net.splitcells.dem.lang.perspective.PerspectiveDocument}.
 */
@JavaLegacyArtifact
public final class Xml {
    private static final Transformer TRANSFORMER = Xml.newTransformer();
    private static final Transformer UNDECLARED_TRANSFORMER = Xml.newTransformer();
    private static final DocumentBuilder ROOT_DOCUMENT_BUILDER = Xml.rootDocumentBuilder();
    private static final Document ROOT_DOCUMENT = ROOT_DOCUMENT_BUILDER.newDocument();

    public static Document document() {
        return ROOT_DOCUMENT_BUILDER.newDocument();
    }

    static {
        Xml.TRANSFORMER.setOutputProperty(INDENT, "yes");
        Xml.TRANSFORMER.setOutputProperty(OMIT_XML_DECLARATION, "no");
        Xml.UNDECLARED_TRANSFORMER.setOutputProperty(INDENT, "yes");
        Xml.UNDECLARED_TRANSFORMER.setOutputProperty(OMIT_XML_DECLARATION, "yes");
    }

    private Xml() {
        throw constructorIllegal();
    }

    private static Transformer newTransformer() {
        try {
            return TransformerFactory.newInstance().newTransformer();
        } catch (TransformerConfigurationException | TransformerFactoryConfigurationError e) {
            throw new RuntimeException(e);
        }
    }

    private static DocumentBuilder rootDocumentBuilder() {
        try {
            final DocumentBuilderFactory rBase = DocumentBuilderFactory.newInstance();
            rBase.setNamespaceAware(true);
            return rBase.newDocumentBuilder();
        } catch (ParserConfigurationException e) {
            throw new RuntimeException(e);
        }
    }

    public static Transformer transformer() {
        return TRANSFORMER;
    }

    public static Element elementWithChildren(NameSpace nameSpace, String name) {
        return ROOT_DOCUMENT.createElement(nameSpace.prefixedName(name));
    }

    public static Element rElement(NameSpace nameSpace, String name) {
        final var rVal = ROOT_DOCUMENT.createElement(nameSpace.prefixedName(name));
        elementWithChildren(rVal, nameSpaceDecleration(nameSpace));
        return rVal;
    }

    public static Element elementWithChildren(NameSpace nameSpace, String name, String value) {
        final var rVal = ROOT_DOCUMENT.createElement(nameSpace.prefixedName(name));
        elementWithChildren(rVal, textNode(value));
        return rVal;
    }

    @Deprecated
    public static Element event(String name, String subject, Node... arguments) {
        final var rVal = elementWithChildren(name);
        final var subjectNode = elementWithChildren("subject");
        subjectNode.appendChild(textNode(subject));
        rVal.appendChild(subjectNode);
        asList(arguments).forEach(arg -> rVal.appendChild(arg));
        return rVal;
    }

    public static Node textNode(String text) {
        return ROOT_DOCUMENT.createTextNode(text);
    }

    public static Element elementWithChildren(String name) {
        try {
            return ROOT_DOCUMENT.createElement(name);
        } catch (RuntimeException e) {
            domsole().append(name);
            throw e;
        }
    }

    public static Element elementWithChildren(Element element, Attr... attributes) {
        for (Attr attribute : attributes) {
            element.setAttributeNode(attribute);
        }
        return element;
    }

    public static Element element2(String name, Stream<Node> nodes) {
        return elementWithChildren(elementWithChildren(name), nodes.collect(toList()));
    }

    public static Element elementWithChildren(String name, Stream<Node> nodes) {
        return elementWithChildren(elementWithChildren(name), nodes.collect(toList()));
    }

    public static Element elementWithChildren(String name, Node... nodes) {
        return elementWithChildren(elementWithChildren(name), nodes);
    }

    public static Element elementWithChildren(String name, NameSpace nameSpace, Node... nodes) {
        return elementWithChildren(name, nameSpace, asList(nodes));
    }

    public static Element elementWithChildren(String name, NameSpace nameSpace, Collection<Node> nodes) {
        final var element = elementWithChildren(name, nameSpace);
        nodes.forEach(node -> element.appendChild(node));
        return element;
    }

    public static Element elementWithChildren(Element element, Node... nodes) {
        return elementWithChildren(element, asList(nodes));
    }

    public static Element elementWithChildren(Element element, Collection<Node> nodes) {
        for (Node node : nodes) {
            if (node != null) {
                element.appendChild(node);
            } else {
                element.appendChild(textNode("null"));
            }
        }
        return element;
    }

    /**
     * Namespace declaration is deprecated, because we need an alternative.
     * Currently there is a problem, when creating single elements with certain namespaces.
     *
     * @param nameSpace
     * @return
     */
    @Deprecated()
    public static Attr nameSpaceDecleration(NameSpace nameSpace) {
        final var rVal = ROOT_DOCUMENT.createAttribute("xmlns:" + nameSpace.defaultPrefix());
        rVal.setNodeValue(nameSpace.uri());
        return rVal;
    }

    public static Attr attribute(NameSpace nameSpace, String name, String value) {
        final var rVal = ROOT_DOCUMENT.createAttribute(nameSpace.prefixedName(name));
        rVal.setNodeValue(value);
        return rVal;
    }

    public static Attr attribute(String name, String value) {
        final var rVal = ROOT_DOCUMENT.createAttribute(name);
        rVal.setNodeValue(value);
        return rVal;
    }

    public static String toDocumentString(Node arg) {
        StreamResult result = new StreamResult(new StringWriter());
        DOMSource source = new DOMSource(arg);
        try {
            TRANSFORMER.transform(source, result);
        } catch (TransformerException e) {
            throw new RuntimeException(e);
        }
        return result.getWriter().toString();
    }

    public static String toPrettyString(Node arg) {
        StreamResult result = new StreamResult(new StringWriter());
        DOMSource source = new DOMSource(arg);
        try {
            TRANSFORMER.transform(source, result);
        } catch (TransformerException e) {
            throw new RuntimeException(e);
        }
        return result.getWriter().toString();
    }

    public static Document parse(Path file) {
        try {
            return ROOT_DOCUMENT_BUILDER.parse(file.toFile());
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public static Document parse(InputStream document) {
        try {
            return ROOT_DOCUMENT_BUILDER.parse(document);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public static Document parse(String content) {
        return parse(new ByteArrayInputStream(content.getBytes()));
    }

    public static String toFlatString(Node arg) {
        return toPrettyString(arg).replaceAll("\\R", "");
    }

    public static String toPrettyWithoutHeaderString(Node arg) {
        StreamResult result = new StreamResult(new StringWriter());
        DOMSource source = new DOMSource(arg);
        try {
            UNDECLARED_TRANSFORMER.transform(source, result);
        } catch (TransformerException e) {
            throw new RuntimeException(e);
        }
        return result.getWriter().toString();
    }

    public static Element directChildElementByName(Element element, String name, NameSpace nameSpace) {
        final var nodeList = element.getChildNodes();
        final var directChildrenByName = directChildElementsByName(element, name, nameSpace)
                .collect(Lists.toList());
        if (directChildrenByName.size() != 1) {
            throw new IllegalArgumentException("Illegal Number of fitting children. Only one fitting child is allowed: " + directChildrenByName.size());
        }
        return directChildrenByName.get(0);
    }

    public static Stream<Element> directChildElementsByName(Element element, String name, NameSpace nameSpace) {
        return directChildElements(element)
                .filter(node -> nameSpace.uri().equals(node.getNamespaceURI()))
                .filter(node -> node.getLocalName().equals(name));
    }

    public static Optional<Element> optionalDirectChildElementsByName(Element element, String name, NameSpace nameSpace) {
        return directChildElements(element)
                .filter(node -> nameSpace.uri().equals(node.getNamespaceURI()))
                .filter(node -> node.getLocalName().equals(name))
                .findFirst();
    }

    public static Stream<Node> directChildNodes(Element element) {
        final var nodeList = element.getChildNodes();
        return IntStream.range(0, nodeList.getLength())
                .mapToObj(i -> nodeList.item(i));
    }

    public static Stream<Element> directChildElements(Element element) {
        return directChildNodes(element)
                .filter(node -> node.getNodeType() == ELEMENT_NODE)
                .map(node -> (Element) node);
    }
}