1   /* Copyright 2002-2025 CS GROUP
2    * Licensed to CS GROUP (CS) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * CS licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *   http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.orekit.files.ccsds.utils.generation;
18  
19  import java.io.IOException;
20  import java.time.ZoneOffset;
21  import java.time.ZonedDateTime;
22  
23  import org.orekit.files.ccsds.definitions.TimeConverter;
24  import org.orekit.files.ccsds.ndm.NdmConstituent;
25  import org.orekit.files.ccsds.section.Header;
26  import org.orekit.files.ccsds.section.HeaderKey;
27  import org.orekit.files.ccsds.section.Segment;
28  import org.orekit.files.ccsds.section.XmlStructureKey;
29  import org.orekit.files.ccsds.utils.ContextBinding;
30  import org.orekit.files.ccsds.utils.FileFormat;
31  import org.orekit.time.AbsoluteDate;
32  import org.orekit.time.DateComponents;
33  import org.orekit.time.DateTimeComponents;
34  import org.orekit.time.TimeComponents;
35  import org.orekit.time.TimeScale;
36  
37  /**
38   * Base class for Navigation Data Message (NDM) files.
39   * @param <H> type of the header
40   * @param <S> type of the segments
41   * @param <F> type of the NDM constituent
42   * @author Luc Maisonobe
43   * @since 11.0
44   */
45  public abstract class AbstractMessageWriter<H extends Header, S extends Segment<?, ?>, F extends NdmConstituent<H, S>>
46      implements MessageWriter<H, S, F> {
47  
48      /** Default value for {@link HeaderKey#ORIGINATOR}. */
49      public static final String DEFAULT_ORIGINATOR = "OREKIT";
50  
51      /** Root element for XML files. */
52      private final String root;
53  
54      /** Key for format version. */
55      private final String formatVersionKey;
56  
57      /** Default format version. */
58      private final double defaultVersion;
59  
60      /** Current context binding. */
61      private ContextBinding context;
62  
63      /** Current converter for dates. */
64      private TimeConverter timeConverter;
65  
66      /** Current format version. */
67      private double version;
68  
69      /**
70       * Constructor used to create a new NDM writer configured with the necessary parameters
71       * to successfully fill in all required fields that aren't part of a standard object.
72       * <p>
73       * If creation date and originator are not present in header, built-in defaults will be used
74       * </p>
75       * @param root root element for XML files
76       * @param formatVersionKey key for format version
77       * @param defaultVersion default format version
78       * @param context context binding (may be reset for each segment)
79       */
80      public AbstractMessageWriter(final String root, final String formatVersionKey,
81                                   final double defaultVersion, final ContextBinding context) {
82  
83          this.root             = root;
84          this.defaultVersion   = defaultVersion;
85          this.formatVersionKey = formatVersionKey;
86          this.version          = defaultVersion;
87  
88          setContext(context);
89  
90      }
91  
92      /** Reset context binding.
93       * @param context context binding to use
94       */
95      public void setContext(final ContextBinding context) {
96          this.context       = context;
97          this.timeConverter = context.getTimeSystem().getConverter(context);
98      }
99  
100     /** Get the current context.
101      * @return current context
102      */
103     public ContextBinding getContext() {
104         return context;
105     }
106 
107     /** Get the current time converter.
108      * @return current time converter
109      */
110     public TimeConverter getTimeConverter() {
111         return timeConverter;
112     }
113 
114     /** Get the default format version.
115      * @return default format version
116      */
117     public double getDefaultVersion() {
118         return defaultVersion;
119     }
120 
121     /** {@inheritDoc} */
122     @Override
123     public void writeHeader(final Generator generator, final H header) throws IOException {
124 
125         final ZonedDateTime zdt = ZonedDateTime.now(ZoneOffset.UTC);
126         final TimeScale     utc = context.getDataContext().getTimeScales().getUTC();
127         final AbsoluteDate date = new AbsoluteDate(zdt.getYear(), zdt.getMonthValue(), zdt.getDayOfMonth(),
128                                                    zdt.getHour(), zdt.getMinute(), zdt.getSecond(),
129                                                    utc);
130 
131         // validate before writing
132         if (header != null) {
133 
134             if (!Double.isNaN(header.getFormatVersion())) {
135                 // save format version for validating segments
136                 version = header.getFormatVersion();
137             }
138 
139             if (header.getCreationDate() == null) {
140                 header.setCreationDate(date);
141             }
142 
143             if (header.getOriginator() == null) {
144                 header.setOriginator(DEFAULT_ORIGINATOR);
145             }
146 
147             header.validate(version);
148 
149         }
150 
151         generator.startMessage(root, formatVersionKey, version);
152 
153         if (generator.getFormat() == FileFormat.XML) {
154             generator.enterSection(XmlStructureKey.header.name());
155         }
156 
157         if (header != null) {
158 
159             // comments are optional
160             generator.writeComments(header.getComments());
161 
162             // classification is optional
163             generator.writeEntry(HeaderKey.CLASSIFICATION.name(), header.getClassification(), null, false);
164 
165         }
166 
167         // creation date is informational only, but mandatory and always in UTC
168         final DateTimeComponents creationDate = ((header == null) ? date : header.getCreationDate()).getComponents(utc);
169         final DateComponents     dc           = creationDate.getDate();
170         final TimeComponents     tc           = creationDate.getTime();
171         generator.writeEntry(HeaderKey.CREATION_DATE.name(),
172                              generator.dateToString(dc.getYear(), dc.getMonth(), dc.getDay(),
173                                                     tc.getHour(), tc.getMinute(), tc.getSecond()),
174                              null, true);
175 
176         // Use built-in default if mandatory originator not present
177         generator.writeEntry(HeaderKey.ORIGINATOR.name(),
178                              header == null ? DEFAULT_ORIGINATOR : header.getOriginator(),
179                              null, true);
180 
181         if (header != null) {
182             generator.writeEntry(HeaderKey.MESSAGE_ID.name(), header.getMessageId(), null, false);
183         }
184 
185         if (generator.getFormat() == FileFormat.XML) {
186             generator.exitSection();
187         }
188 
189         // add an empty line for presentation
190         generator.newLine();
191 
192         if (generator.getFormat() == FileFormat.XML) {
193             generator.enterSection(XmlStructureKey.body.name());
194         }
195 
196     }
197 
198     /** {@inheritDoc} */
199     @Override
200     public void writeSegment(final Generator generator, final S segment) throws IOException {
201 
202         // validate before writing
203         segment.getMetadata().validate(version);
204         segment.getData().validate(version);
205 
206         if (generator.getFormat() == FileFormat.XML) {
207             generator.enterSection(XmlStructureKey.segment.name());
208         }
209         writeSegmentContent(generator, version, segment);
210         if (generator.getFormat() == FileFormat.XML) {
211             generator.exitSection();
212         }
213 
214     }
215 
216     /** Write one segment content (without XML wrapping).
217      * @param generator generator to use for producing output
218      * @param formatVersion format version to use
219      * @param segment segment to write
220      * @throws IOException if any buffer writing operations fails
221      */
222     protected abstract void writeSegmentContent(Generator generator, double formatVersion, S segment) throws IOException;
223 
224     /** {@inheritDoc} */
225     @Override
226     public void writeFooter(final Generator generator) throws IOException {
227         if (generator.getFormat() == FileFormat.XML) {
228             generator.exitSection();
229         }
230         generator.endMessage(root);
231     }
232 
233     /** {@inheritDoc} */
234     @Override
235     public String getRoot() {
236         return root;
237     }
238 
239     /** {@inheritDoc} */
240     @Override
241     public String getFormatVersionKey() {
242         return formatVersionKey;
243     }
244 
245     /** {@inheritDoc} */
246     @Override
247     public double getVersion() {
248         return version;
249     }
250 
251 }