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.util.List;
21  
22  import org.orekit.files.ccsds.utils.FileFormat;
23  import org.orekit.utils.AccurateFormatter;
24  import org.orekit.utils.Formatter;
25  import org.orekit.utils.units.Unit;
26  
27  /** Generator for eXtended Markup Language CCSDS messages.
28   * @author Luc Maisonobe
29   * @since 11.0
30   */
31  public class XmlGenerator extends AbstractGenerator {
32  
33      /** Default number of space for each indentation level. */
34      public static final int DEFAULT_INDENT = 2;
35  
36      /** Name of the units attribute. */
37      public static final String UNITS = "units";
38  
39      /** NDM/XML version 3 location.
40       * @since 12.0
41       */
42      public static final String NDM_XML_V3_SCHEMA_LOCATION = "https://sanaregistry.org/r/ndmxml_unqualified/ndmxml-3.0.0-master-3.0.xsd";
43  
44      /** XML prolog. */
45      private static final String PROLOG = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>%n";
46  
47      /** Root element start tag, with schema.
48       * @since 12.0
49       */
50      private static final String ROOT_START_WITH_SCHEMA = "<%s xmlns:xsi=\"%s\" xsi:noNamespaceSchemaLocation=\"%s\" id=\"%s\" version=\"%.1f\">%n";
51  
52      /** Root element start tag, without schema.
53       * @since 12.0
54       */
55      private static final String ROOT_START_WITHOUT_SCHEMA = "<%s id=\"%s\" version=\"%.1f\">%n";
56  
57      /** Constant for XML schema instance.
58       * @since 12.0
59       */
60      private static final String XMLNS_XSI = "http://www.w3.org/2001/XMLSchema-instance";
61  
62      /** Element end tag.
63       * @since 12.0
64       */
65      private static final String START_TAG_WITH_SCHEMA = "<%s xmlns:xsi=\"%s\" xsi:noNamespaceSchemaLocation=\"%s\">%n";
66  
67      /** Element end tag.
68       * @since 12.0
69       */
70      private static final String START_TAG_WITHOUT_SCHEMA = "<%s>%n";
71  
72      /** Element end tag. */
73      private static final String END_TAG = "</%s>%n";
74  
75      /** Leaf element format without attributes. */
76      private static final String LEAF_0_ATTRIBUTES = "<%s>%s</%s>%n";
77  
78      /** Leaf element format with one attribute. */
79      private static final String LEAF_1_ATTRIBUTE = "<%s %s=\"%s\">%s</%s>%n";
80  
81      /** Leaf element format with two attributes. */
82      private static final String LEAF_2_ATTRIBUTES = "<%s %s=\"%s\" %s=\"%s\">%s</%s>%n";
83  
84      /** Comment key. */
85      private static final String COMMENT = "COMMENT";
86  
87      /** Schema location. */
88      private final String schemaLocation;
89  
90      /** Indentation size. */
91      private final int indentation;
92  
93      /** Nesting level. */
94      private int level;
95  
96      /** Simple constructor.
97       * @param output destination of generated output
98       * @param indentation number of space for each indentation level
99       * @param outputName output name for error messages
100      * @param maxRelativeOffset maximum offset in seconds to use relative dates
101      * (if a date is too far from reference, it will be displayed as calendar elements)
102      * @param formatter used to convert doubles and dates to string
103      * @param writeUnits if true, units must be written
104      * @param schemaLocation schema location to use, may be null
105      * @see #DEFAULT_INDENT
106      * @see #NDM_XML_V3_SCHEMA_LOCATION
107      * @throws IOException if an I/O error occurs.
108      */
109     public XmlGenerator(final Appendable output, final int indentation,
110                         final String outputName, final double maxRelativeOffset,
111                         final boolean writeUnits, final String schemaLocation,
112                         final Formatter formatter) throws IOException {
113         super(output, outputName, maxRelativeOffset, writeUnits, formatter);
114         this.schemaLocation = schemaLocation;
115         this.indentation    = indentation;
116         this.level          = 0;
117         writeRawData(String.format(Formatter.STANDARDIZED_LOCALE, PROLOG));
118     }
119 
120     /** Simple constructor.
121      * @param output destination of generated output
122      * @param indentation number of space for each indentation level
123      * @param outputName output name for error messages
124      * @param maxRelativeOffset maximum offset in seconds to use relative dates
125      * (if a date is too far from reference, it will be displayed as calendar elements)
126      * @param writeUnits if true, units must be written
127      * @param schemaLocation schema location to use, may be null
128      * @see #DEFAULT_INDENT
129      * @see #NDM_XML_V3_SCHEMA_LOCATION
130      * @throws IOException if an I/O error occurs.
131      */
132     public XmlGenerator(final Appendable output, final int indentation,
133                         final String outputName, final double maxRelativeOffset,
134                         final boolean writeUnits, final String schemaLocation) throws IOException {
135         this(output, indentation, outputName, maxRelativeOffset, writeUnits, schemaLocation, new AccurateFormatter());
136     }
137 
138 
139     /** {@inheritDoc} */
140     @Override
141     public FileFormat getFormat() {
142         return FileFormat.XML;
143     }
144 
145     /** {@inheritDoc} */
146     @Override
147     public void startMessage(final String root, final String messageTypeKey, final double version) throws IOException {
148         indent();
149         if (schemaLocation == null || level > 0) {
150             writeRawData(String.format(Formatter.STANDARDIZED_LOCALE, ROOT_START_WITHOUT_SCHEMA,
151                                        root, messageTypeKey, version));
152         } else {
153             writeRawData(String.format(Formatter.STANDARDIZED_LOCALE, ROOT_START_WITH_SCHEMA,
154                                        root, XMLNS_XSI, schemaLocation, messageTypeKey, version));
155         }
156         ++level;
157     }
158 
159     /** {@inheritDoc} */
160     @Override
161     public void endMessage(final String root) throws IOException {
162         --level;
163         indent();
164         writeRawData(String.format(Formatter.STANDARDIZED_LOCALE, END_TAG,
165                                    root));
166     }
167 
168     /** {@inheritDoc} */
169     @Override
170     public void writeComments(final List<String> comments) throws IOException {
171         for (final String comment : comments) {
172             writeEntry(COMMENT, comment, null, false);
173         }
174     }
175 
176     /** Write an element with one attribute.
177      * @param name tag name
178      * @param value element value
179      * @param attributeName attribute name
180      * @param attributeValue attribute value
181      * @throws IOException if an I/O error occurs.
182      */
183     public void writeOneAttributeElement(final String name, final String value,
184                                          final String attributeName, final String attributeValue)
185         throws IOException {
186         indent();
187         writeRawData(String.format(Formatter.STANDARDIZED_LOCALE, LEAF_1_ATTRIBUTE,
188                                    name, attributeName, attributeValue, value, name));
189     }
190 
191     /** Write an element with two attributes.
192      * @param name tag name
193      * @param value element value
194      * @param attribute1Name attribute 1 name
195      * @param attribute1Value attribute 1 value
196      * @param attribute2Name attribute 2 name
197      * @param attribute2Value attribute 2 value
198      * @throws IOException if an I/O error occurs.
199      */
200     public void writeTwoAttributesElement(final String name, final String value,
201                                           final String attribute1Name, final String attribute1Value,
202                                           final String attribute2Name, final String attribute2Value)
203         throws IOException {
204         indent();
205         writeRawData(String.format(Formatter.STANDARDIZED_LOCALE, LEAF_2_ATTRIBUTES,
206                                    name,
207                                    attribute1Name, attribute1Value, attribute2Name, attribute2Value,
208                                    value, name));
209     }
210 
211     /** {@inheritDoc} */
212     @Override
213     public void writeEntry(final String key, final String value, final Unit unit, final boolean mandatory) throws IOException {
214         if (value == null) {
215             complain(key, mandatory);
216         } else {
217             indent();
218             if (writeUnits(unit)) {
219                 writeRawData(String.format(Formatter.STANDARDIZED_LOCALE, LEAF_1_ATTRIBUTE,
220                                            key, UNITS, siToCcsdsName(unit.getName()), value, key));
221             } else {
222                 writeRawData(String.format(Formatter.STANDARDIZED_LOCALE, LEAF_0_ATTRIBUTES,
223                                            key, value, key));
224             }
225         }
226     }
227 
228     /** {@inheritDoc} */
229     @Override
230     public void enterSection(final String name) throws IOException {
231         indent();
232         if (schemaLocation != null && level == 0) {
233             // top level tag for ndm messages (it is called before enterMessage)
234             writeRawData(String.format(Formatter.STANDARDIZED_LOCALE, START_TAG_WITH_SCHEMA,
235                                        name, XMLNS_XSI, schemaLocation));
236         } else {
237             writeRawData(String.format(Formatter.STANDARDIZED_LOCALE, START_TAG_WITHOUT_SCHEMA, name));
238         }
239         ++level;
240         super.enterSection(name);
241     }
242 
243     /** {@inheritDoc} */
244     @Override
245     public String exitSection() throws IOException {
246         final String name = super.exitSection();
247         --level;
248         indent();
249         writeRawData(String.format(Formatter.STANDARDIZED_LOCALE, END_TAG, name));
250         return name;
251     }
252 
253     /** Indent line.
254      * @throws IOException if an I/O error occurs.
255      */
256     private void indent() throws IOException {
257         for (int i = 0; i < level * indentation; ++i) {
258             writeRawData(' ');
259         }
260     }
261 
262 }