XmlGenerator.java

/* Copyright 2002-2024 CS GROUP
 * Licensed to CS GROUP (CS) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * CS licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.orekit.files.ccsds.utils.generation;

import java.io.IOException;
import java.util.List;

import org.orekit.files.ccsds.utils.FileFormat;
import org.orekit.utils.AccurateFormatter;
import org.orekit.utils.units.Unit;

/** Generator for eXtended Markup Language CCSDS messages.
 * @author Luc Maisonobe
 * @since 11.0
 */
public class XmlGenerator extends AbstractGenerator {

    /** Default number of space for each indentation level. */
    public static final int DEFAULT_INDENT = 2;

    /** Name of the units attribute. */
    public static final String UNITS = "units";

    /** NDM/XML version 3 location.
     * @since 12.0
     */
    public static final String NDM_XML_V3_SCHEMA_LOCATION = "https://sanaregistry.org/r/ndmxml_unqualified/ndmxml-3.0.0-master-3.0.xsd";

    /** XML prolog. */
    private static final String PROLOG = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>%n";

    /** Root element start tag, with schema.
     * @since 12.0
     */
    private static final String ROOT_START_WITH_SCHEMA = "<%s xmlns:xsi=\"%s\" xsi:noNamespaceSchemaLocation=\"%s\" id=\"%s\" version=\"%.1f\">%n";

    /** Root element start tag, without schema.
     * @since 12.0
     */
    private static final String ROOT_START_WITHOUT_SCHEMA = "<%s id=\"%s\" version=\"%.1f\">%n";

    /** Constant for XML schema instance.
     * @since 12.0
     */
    private static final String XMLNS_XSI = "http://www.w3.org/2001/XMLSchema-instance";

    /** Element end tag.
     * @since 12.0
     */
    private static final String START_TAG_WITH_SCHEMA = "<%s xmlns:xsi=\"%s\" xsi:noNamespaceSchemaLocation=\"%s\">%n";

    /** Element end tag.
     * @since 12.0
     */
    private static final String START_TAG_WITHOUT_SCHEMA = "<%s>%n";

    /** Element end tag. */
    private static final String END_TAG = "</%s>%n";

    /** Leaf element format without attributes. */
    private static final String LEAF_0_ATTRIBUTES = "<%s>%s</%s>%n";

    /** Leaf element format with one attribute. */
    private static final String LEAF_1_ATTRIBUTE = "<%s %s=\"%s\">%s</%s>%n";

    /** Leaf element format with two attributes. */
    private static final String LEAF_2_ATTRIBUTES = "<%s %s=\"%s\" %s=\"%s\">%s</%s>%n";

    /** Comment key. */
    private static final String COMMENT = "COMMENT";

    /** Schema location. */
    private final String schemaLocation;

    /** Indentation size. */
    private final int indentation;

    /** Nesting level. */
    private int level;

    /** Simple constructor.
     * @param output destination of generated output
     * @param indentation number of space for each indentation level
     * @param outputName output name for error messages
     * @param maxRelativeOffset maximum offset in seconds to use relative dates
     * (if a date is too far from reference, it will be displayed as calendar elements)
     * @param writeUnits if true, units must be written
     * @param schemaLocation schema location to use, may be null
     * @see #DEFAULT_INDENT
     * @see #NDM_XML_V3_SCHEMA_LOCATION
     * @throws IOException if an I/O error occurs.
     */
    public XmlGenerator(final Appendable output, final int indentation,
                        final String outputName, final double maxRelativeOffset,
                        final boolean writeUnits, final String schemaLocation) throws IOException {
        super(output, outputName, maxRelativeOffset, writeUnits);
        this.schemaLocation = schemaLocation;
        this.indentation    = indentation;
        this.level          = 0;
        writeRawData(String.format(AccurateFormatter.STANDARDIZED_LOCALE, PROLOG));
    }

    /** {@inheritDoc} */
    @Override
    public FileFormat getFormat() {
        return FileFormat.XML;
    }

    /** {@inheritDoc} */
    @Override
    public void startMessage(final String root, final String messageTypeKey, final double version) throws IOException {
        indent();
        if (schemaLocation == null || level > 0) {
            writeRawData(String.format(AccurateFormatter.STANDARDIZED_LOCALE, ROOT_START_WITHOUT_SCHEMA,
                                       root, messageTypeKey, version));
        } else {
            writeRawData(String.format(AccurateFormatter.STANDARDIZED_LOCALE, ROOT_START_WITH_SCHEMA,
                                       root, XMLNS_XSI, schemaLocation, messageTypeKey, version));
        }
        ++level;
    }

    /** {@inheritDoc} */
    @Override
    public void endMessage(final String root) throws IOException {
        --level;
        indent();
        writeRawData(String.format(AccurateFormatter.STANDARDIZED_LOCALE, END_TAG,
                                   root));
    }

    /** {@inheritDoc} */
    @Override
    public void writeComments(final List<String> comments) throws IOException {
        for (final String comment : comments) {
            writeEntry(COMMENT, comment, null, false);
        }
    }

    /** Write an element with one attribute.
     * @param name tag name
     * @param value element value
     * @param attributeName attribute name
     * @param attributeValue attribute value
     * @throws IOException if an I/O error occurs.
     */
    public void writeOneAttributeElement(final String name, final String value,
                                         final String attributeName, final String attributeValue)
        throws IOException {
        indent();
        writeRawData(String.format(AccurateFormatter.STANDARDIZED_LOCALE, LEAF_1_ATTRIBUTE,
                                   name, attributeName, attributeValue, value, name));
    }

    /** Write an element with two attributes.
     * @param name tag name
     * @param value element value
     * @param attribute1Name attribute 1 name
     * @param attribute1Value attribute 1 value
     * @param attribute2Name attribute 2 name
     * @param attribute2Value attribute 2 value
     * @throws IOException if an I/O error occurs.
     */
    public void writeTwoAttributesElement(final String name, final String value,
                                          final String attribute1Name, final String attribute1Value,
                                          final String attribute2Name, final String attribute2Value)
        throws IOException {
        indent();
        writeRawData(String.format(AccurateFormatter.STANDARDIZED_LOCALE, LEAF_2_ATTRIBUTES,
                                   name,
                                   attribute1Name, attribute1Value, attribute2Name, attribute2Value,
                                   value, name));
    }

    /** {@inheritDoc} */
    @Override
    public void writeEntry(final String key, final String value, final Unit unit, final boolean mandatory) throws IOException {
        if (value == null) {
            complain(key, mandatory);
        } else {
            indent();
            if (writeUnits(unit)) {
                writeRawData(String.format(AccurateFormatter.STANDARDIZED_LOCALE, LEAF_1_ATTRIBUTE,
                                           key, UNITS, siToCcsdsName(unit.getName()), value, key));
            } else {
                writeRawData(String.format(AccurateFormatter.STANDARDIZED_LOCALE, LEAF_0_ATTRIBUTES,
                                           key, value, key));
            }
        }
    }

    /** {@inheritDoc} */
    @Override
    public void enterSection(final String name) throws IOException {
        indent();
        if (schemaLocation != null && level == 0) {
            // top level tag for ndm messages (it is called before enterMessage)
            writeRawData(String.format(AccurateFormatter.STANDARDIZED_LOCALE, START_TAG_WITH_SCHEMA,
                                       name, XMLNS_XSI, schemaLocation));
        } else {
            writeRawData(String.format(AccurateFormatter.STANDARDIZED_LOCALE, START_TAG_WITHOUT_SCHEMA, name));
        }
        ++level;
        super.enterSection(name);
    }

    /** {@inheritDoc} */
    @Override
    public String exitSection() throws IOException {
        final String name = super.exitSection();
        --level;
        indent();
        writeRawData(String.format(AccurateFormatter.STANDARDIZED_LOCALE, END_TAG, name));
        return name;
    }

    /** Indent line.
     * @throws IOException if an I/O error occurs.
     */
    private void indent() throws IOException {
        for (int i = 0; i < level * indentation; ++i) {
            writeRawData(' ');
        }
    }

}