XmlWriter.java

/**
 * Copyright (c) 2022, RTE (http://www.rte-france.com)
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 * SPDX-License-Identifier: MPL-2.0
 */
package com.powsybl.commons.xml;

import com.powsybl.commons.PowsyblException;
import com.powsybl.commons.exceptions.UncheckedXmlStreamException;
import com.powsybl.commons.io.AbstractTreeDataWriter;
import org.apache.commons.lang3.StringUtils;

import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamWriter;
import java.io.OutputStream;
import java.nio.charset.Charset;
import java.util.*;
import java.util.stream.Collectors;

/**
 * @author Geoffroy Jamgotchian {@literal <geoffroy.jamgotchian at rte-france.com>}
 */
public class XmlWriter extends AbstractTreeDataWriter {

    private final XMLStreamWriter writer;

    private String currentNodeNamespace;
    private String currentNodeName;
    private final List<String> names = new ArrayList<>();
    private final List<String> values = new ArrayList<>();
    private final List<String> prefixes = new ArrayList<>();
    private final List<String> namespaces = new ArrayList<>();
    private final Map<String, Namespace> extensionNamespaces = new LinkedHashMap<>();

    public XmlWriter(OutputStream os, String indent, Charset charset, String rootNamespaceURI, String rootPrefix) throws XMLStreamException {
        this.writer = XmlUtil.initializeWriter(!StringUtils.isEmpty(indent), indent,
                Objects.requireNonNull(os), Objects.requireNonNull(charset));
        this.namespaces.add(Objects.requireNonNull(rootNamespaceURI));
        this.prefixes.add(Objects.requireNonNull(rootPrefix));
    }

    @Override
    public void writeStartNodes() {
        // nothing to do
    }

    @Override
    public void writeEndNodes() {
        // nothing to do
    }

    @Override
    public void writeStartNode(String namespace, String name) {
        try {
            if (currentNodeName != null) {
                writePrefixes();
                writer.writeStartElement(currentNodeNamespace, currentNodeName);
                flushAttributes();
            }
            currentNodeNamespace = namespace;
            currentNodeName = name;
        } catch (XMLStreamException e) {
            throw new UncheckedXmlStreamException(e);
        }
    }

    private void writePrefixes() throws XMLStreamException {
        for (int i = 0; i < prefixes.size(); i++) {
            writer.setPrefix(prefixes.get(i), namespaces.get(i));
        }
    }

    private void flushAttributes() throws XMLStreamException {
        for (int i = 0; i < prefixes.size(); i++) {
            writer.writeNamespace(prefixes.get(i), namespaces.get(i));
        }
        prefixes.clear();
        namespaces.clear();
        for (int i = 0; i < names.size(); i++) {
            writer.writeAttribute(names.get(i), values.get(i));
        }
        names.clear();
        values.clear();
    }

    @Override
    public void writeEndNode() {
        try {
            if (currentNodeName != null) {
                writePrefixes();
                writer.writeEmptyElement(currentNodeNamespace, currentNodeName);
                flushAttributes();
                currentNodeNamespace = null;
                currentNodeName = null;
            } else {
                writer.writeEndElement();
            }
        } catch (XMLStreamException e) {
            throw new UncheckedXmlStreamException(e);
        }
    }

    @Override
    public void writeNamespace(String prefix, String namespace) {
        prefixes.add(prefix);
        namespaces.add(namespace);
    }

    @Override
    public void writeNodeContent(String value) {
        try {
            if (currentNodeName != null) {
                writePrefixes();
                writer.writeStartElement(currentNodeNamespace, currentNodeName);
                flushAttributes();
                currentNodeName = null;
                currentNodeNamespace = null;
            }
            writer.writeCharacters(value);
        } catch (XMLStreamException e) {
            throw new UncheckedXmlStreamException(e);
        }
    }

    @Override
    public void writeStringAttribute(String name, String value) {
        if (value != null) {
            names.add(name);
            values.add(value);
        }
    }

    @Override
    public void writeFloatAttribute(String name, float value) {
        if (!Float.isNaN(value)) {
            names.add(name);
            values.add(Float.toString(value));
        }
    }

    @Override
    public void writeDoubleAttribute(String name, double value) {
        if (!Double.isNaN(value)) {
            names.add(name);
            values.add(Double.toString(value));
        }
    }

    @Override
    public void writeDoubleAttribute(String name, double value, double absentValue) {
        if (!Double.isNaN(value) && value != absentValue) {
            names.add(name);
            values.add(Double.toString(value));
        }
    }

    @Override
    public void writeIntAttribute(String name, int value) {
        names.add(name);
        values.add(Integer.toString(value));
    }

    @Override
    public void writeIntAttribute(String name, int value, int absentValue) {
        if (value != absentValue) {
            names.add(name);
            values.add(Integer.toString(value));
        }
    }

    @Override
    public void writeIntArrayAttribute(String name, Collection<Integer> values) {
        if (!values.isEmpty()) {
            writeStringAttribute(name, values.stream()
                    .map(i -> Integer.toString(i))
                    .collect(Collectors.joining(",")));
        }
    }

    @Override
    public void writeStringArrayAttribute(String name, Collection<String> values) {
        if (!values.isEmpty()) {
            writeStringAttribute(name, String.join(",", values));
        }
    }

    @Override
    public <E extends Enum<E>> void writeEnumAttribute(String name, E value) {
        if (value != null) {
            names.add(name);
            values.add(value.name());
        }
    }

    @Override
    public void writeBooleanAttribute(String name, boolean value) {
        names.add(name);
        values.add(Boolean.toString(value));
    }

    @Override
    public void writeBooleanAttribute(String name, boolean value, boolean absentValue) {
        if (value != absentValue) {
            names.add(name);
            values.add(Boolean.toString(value));
        }
    }

    @Override
    public void close() {
        try {
            writer.writeEndDocument();
            writer.close();
        } catch (XMLStreamException e) {
            throw new UncheckedXmlStreamException(e);
        }
    }

    @Override
    public void setVersions(Map<String, String> versions) {
        Objects.requireNonNull(versions).keySet()
                .stream()
                .map(extensionName -> {
                    Namespace namespace = extensionNamespaces.get(extensionName);
                    if (namespace == null) {
                        throw new PowsyblException("No namespace known for extension " + extensionName);
                    }
                    return namespace;
                })
                .forEach(namespace -> {
                    prefixes.add(namespace.prefix());
                    namespaces.add(namespace.uri());
                });
    }

    public void setExtensionNamespace(String extensionName, String namespaceUri, String namespacePrefix) {
        extensionNamespaces.put(extensionName, new Namespace(namespaceUri, namespacePrefix));
    }

    private record Namespace(String uri, String prefix) {
    }
}