CdmMessageWriter.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.ndm.cdm;

import java.io.IOException;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;

import org.orekit.files.ccsds.definitions.TimeConverter;
import org.orekit.files.ccsds.section.HeaderKey;
import org.orekit.files.ccsds.section.Segment;
import org.orekit.files.ccsds.section.XmlStructureKey;
import org.orekit.files.ccsds.utils.ContextBinding;
import org.orekit.files.ccsds.utils.FileFormat;
import org.orekit.files.ccsds.utils.generation.Generator;
import org.orekit.files.ccsds.utils.generation.MessageWriter;
import org.orekit.time.AbsoluteDate;
import org.orekit.time.DateComponents;
import org.orekit.time.DateTimeComponents;
import org.orekit.time.TimeComponents;
import org.orekit.time.TimeScale;

/** Cdm message writer.
 * @author Melina Vanel
 * @since 11.2
 */
public abstract class CdmMessageWriter implements MessageWriter<CdmHeader, CdmSegment, Cdm> {

    /** Default value for {@link HeaderKey#ORIGINATOR}. */
    public static final String DEFAULT_ORIGINATOR = "OREKIT";

    /** Root element for XML files. */
    private final String root;

    /** Key for format version. */
    private final String formatVersionKey;

    /** Default format version. */
    private final double defaultVersion;

    /** Current context binding. */
    private ContextBinding context;

    /** Current converter for dates. */
    private TimeConverter timeConverter;

    /** Current format version. */
    private double version;

    /** Boolean to ensure relative metadata will be written only once. */
    private boolean isrelativemetadataWritten;

    /**
     * Constructor used to create a new NDM writer configured with the necessary parameters
     * to successfully fill in all required fields that aren't part of a standard object.
     * <p>
     * If creation date and originator are not present in header, built-in defaults will be used
     * </p>
     * @param root root element for XML files
     * @param formatVersionKey key for format version
     * @param defaultVersion default format version
     * @param context context binding (may be reset for each segment)
     */
    public CdmMessageWriter(final String root, final String formatVersionKey,
                                 final double defaultVersion, final ContextBinding context) {

        this.root                      = root;
        this.defaultVersion            = defaultVersion;
        this.formatVersionKey          = formatVersionKey;
        this.version                   = defaultVersion;
        this.isrelativemetadataWritten = false;

        setContext(context);

    }

    /** Reset context binding.
     * @param context context binding to use
     */
    public void setContext(final ContextBinding context) {
        this.context       = context;
        this.timeConverter = context.getTimeSystem().getConverter(context);
    }

    /** Get the current context.
     * @return current context
     */
    public ContextBinding getContext() {
        return context;
    }

    /** Get the current time converter.
     * @return current time converter
     */
    public TimeConverter getTimeConverter() {
        return timeConverter;
    }

    /** Get the default format version.
     * @return default format version
     */
    public double getDefaultVersion() {
        return defaultVersion;
    }

    /** {@inheritDoc} */
    @Override
    public void writeHeader(final Generator generator, final CdmHeader header) throws IOException {

        final ZonedDateTime zdt = ZonedDateTime.now(ZoneOffset.UTC);
        final TimeScale     utc = context.getDataContext().getTimeScales().getUTC();
        final AbsoluteDate date = new AbsoluteDate(zdt.getYear(), zdt.getMonthValue(), zdt.getDayOfMonth(),
                                                   zdt.getHour(), zdt.getMinute(), zdt.getSecond(),
                                                   utc);

        // validate before writing
        if (header != null) {

            if (!Double.isNaN(header.getFormatVersion())) {
                // save format version for validating segments
                version = header.getFormatVersion();
            }

            if (header.getCreationDate() == null) {
                header.setCreationDate(date);
            }

            if (header.getOriginator() == null) {
                header.setOriginator(DEFAULT_ORIGINATOR);
            }

            header.validate(version);

        }

        generator.startMessage(root, formatVersionKey, version);

        if (generator.getFormat() == FileFormat.XML) {
            generator.enterSection(XmlStructureKey.header.name());
        }

        // comments are optional
        if (header != null) {
            generator.writeComments(header.getComments());
        }

        // creation date is informational only, but mandatory and always in UTC
        final DateTimeComponents creationDate = ((header == null) ? date : header.getCreationDate()).getComponents(utc);
        final DateComponents     dc           = creationDate.getDate();
        final TimeComponents     tc           = creationDate.getTime();
        generator.writeEntry(HeaderKey.CREATION_DATE.name(),
                             generator.dateToString(dc.getYear(), dc.getMonth(), dc.getDay(),
                                                    tc.getHour(), tc.getMinute(), tc.getSecond()),
                             null, true);

        // Use built-in default if mandatory originator not present
        generator.writeEntry(HeaderKey.ORIGINATOR.name(),
                             header == null ? DEFAULT_ORIGINATOR : header.getOriginator(),
                             null, true);

        if (header != null) {
            generator.writeEntry(CdmHeaderKey.MESSAGE_FOR.name(), header.getMessageFor(), null, false);
        }

        if (header != null) {
            generator.writeEntry(HeaderKey.MESSAGE_ID.name(), header.getMessageId(), null, false);
        }

        if (generator.getFormat() == FileFormat.XML) {
            generator.exitSection();
        }

        // add an empty line for presentation
        generator.newLine();

        if (generator.getFormat() == FileFormat.XML) {
            generator.enterSection(XmlStructureKey.body.name());
        }

    }

    /** {@inheritDoc} */
    @Override
    public void writeSegment(final Generator generator, final CdmSegment segment) throws IOException {

        // validate before writing
        segment.getMetadata().validate(version);
        segment.getData().validate(version);

        // relative metadata should only be written once after the header at the beginning of the body
        if (!isrelativemetadataWritten) {
            writeRelativeMetadataContent(generator, version, segment.getMetadata().getRelativeMetadata());
            isrelativemetadataWritten = true;
        }

        if (generator.getFormat() == FileFormat.XML) {
            generator.enterSection(XmlStructureKey.segment.name());
        }
        writeSegmentContent(generator, version, segment);
        if (generator.getFormat() == FileFormat.XML) {
            generator.exitSection();
        }

    }

    /** Write RelativeMetadata part only once after header.
     * @param generator generator to use for producing output
     * @param formatVersion format version to use
     * @param relativeMetadata relative metadata to write
     * @throws IOException if any buffer writing operations fails
     */
    public abstract void writeRelativeMetadataContent(Generator generator, double formatVersion,
                                                      CdmRelativeMetadata relativeMetadata) throws IOException;

    /** Write one segment content (without XML wrapping).
     * @param generator generator to use for producing output
     * @param formatVersion format version to use
     * @param segment segment to write
     * @throws IOException if any buffer writing operations fails
     */
    public abstract void writeSegmentContent(Generator generator, double formatVersion,
                                             Segment<CdmMetadata, CdmData> segment) throws IOException;

    /** {@inheritDoc} */
    @Override
    public void writeFooter(final Generator generator) throws IOException {
        if (generator.getFormat() == FileFormat.XML) {
            generator.exitSection();
        }
        generator.endMessage(root);
    }

    /** {@inheritDoc} */
    @Override
    public String getRoot() {
        return root;
    }

    /** {@inheritDoc} */
    @Override
    public String getFormatVersionKey() {
        return formatVersionKey;
    }

    /** {@inheritDoc} */
    @Override
    public double getVersion() {
        return version;
    }

}