I’m currently writting NDM files and while doing my unit validation I found an interesting bug.
My unit test is easy and consist in validating that an empty NDM can be generated by Orekit.
Bug detail
While generating my empty NDM I was supprised to see that only the XML header was generated and not the <ndm></ndm> block (even if empty). This block is mandatory according to 8.12.5 of CCSDS blue book.
<?xml version="1.0" encoding="UTF-8"?>
So I tried to parse this file using Orekit and encountered the following exception.
org.orekit.errors.OrekitException: Premature end of file.
at org.orekit.files.ccsds.utils.lexical.XmlLexicalAnalyzer.accept(XmlLexicalAnalyzer.java:95)
at org.orekit.files.ccsds.utils.parsing.AbstractMessageParser.parseMessage(AbstractMessageParser.java:154)
at org.orekit.files.ccsds.ndm.NdmWriterTest.testEmpty(NdmWriterTest.java:61)
at java.lang.reflect.Method.invoke(Method.java:498)
at java.util.ArrayList.forEach(ArrayList.java:1259)
at java.util.ArrayList.forEach(ArrayList.java:1259)
Caused by: org.xml.sax.SAXParseException; lineNumber: 2; columnNumber: 1; Premature end of file.
How to reproduce?
Please find below a small example to reproduce the issue. The NDM is generated with Orekit and parsed again with Orekit
@Test
public void testEmpty() throws IOException {
StringBuffer stringBuffer = new StringBuffer();
new WriterBuilder(DataContext.getDefault()).buildNdmWriter()
.writeMessage(new XmlGenerator(stringBuffer, 0, "empty.xml", 0., false, XmlGenerator.NDM_XML_V3_SCHEMA_LOCATION),
new Ndm(new ArrayList<>(), new ArrayList<>()));
Ndm ndm = new ParserBuilder().buildNdmParser().parseMessage(new DataSource("empty.xml", () -> new StringReader(stringBuffer.toString())));
Assertions.assertTrue(ndm.getComments().isEmpty());
Assertions.assertTrue(ndm.getConstituents().isEmpty());
}
I tried to generate a NDM with 1 OPM and 1 APM and found that the </ndm> key is missing at the end of the file. However, it is mandatory.
So, when I try to read this file with Orekit again, I have the following exception (meaning that the key is missing.
org.orekit.errors.OrekitException: XML document structures must start and end within the same entity.
at org.orekit.files.ccsds.utils.lexical.XmlLexicalAnalyzer.accept(XmlLexicalAnalyzer.java:95)
at org.orekit.files.ccsds.utils.parsing.AbstractMessageParser.parseMessage(AbstractMessageParser.java:154)
at org.orekit.files.ccsds.ndm.NdmWriterTest.testBugMissingNdmEndKey(NdmWriterTest.java:88)
at java.lang.reflect.Method.invoke(Method.java:498)
at java.util.ArrayList.forEach(ArrayList.java:1259)
at java.util.ArrayList.forEach(ArrayList.java:1259)
Caused by: org.xml.sax.SAXParseException; lineNumber: 77; columnNumber: 1; XML document structures must start and end within the same entity.
How to reproduce?
Please find below and example explaining how to reproduce.
@Test
public void testBugMissingNdmEndKey() throws IOException {
ParserBuilder parserBuilder = new ParserBuilder(DataContext.getDefault()).withConventions(IERSConventions.IERS_2010)
.withMu(Constants.WGS84_EARTH_MU)
.withDefaultMass(Propagator.DEFAULT_MASS);
final String opmName = "/ccsds/odm/opm/OPMExample1.txt";
final Opm opm = parserBuilder.buildOpmParser().parseMessage(new DataSource(opmName, () -> NdmWriterTest.class.getResourceAsStream(opmName)));
final String apmName = "/ccsds/adm/apm/APMExample01.txt";
final Apm apm = parserBuilder.buildApmParser().parseMessage(new DataSource(apmName, () -> NdmWriterTest.class.getResourceAsStream(apmName)));
List<NdmConstituent<?, ?>> constituents = new ArrayList<>();
constituents.add(opm);
constituents.add(apm);
final Ndm initial = new Ndm(new ArrayList<>(), constituents);
StringBuffer stringBuffer = new StringBuffer();
new WriterBuilder(DataContext.getDefault()).buildNdmWriter()
.writeMessage(new XmlGenerator(stringBuffer, 0, "empty.xml", 0., false, XmlGenerator.NDM_XML_V3_SCHEMA_LOCATION),
initial);
Ndm rebuilt = new ParserBuilder().buildNdmParser().parseMessage(new DataSource("empty.xml", () -> new StringReader(stringBuffer.toString())));
Assertions.assertEquals(2, rebuilt.getConstituents().size());
}
From what i remember, i’m not sure if it is explicitely mentioned that the block should be added even for empty/single content. I assumed that it was only required when containing multiple data but i would have to check again.
However even in that case, i guess it should ideally return an Optional (though this would be another feature) that could be empty.
For me they are three clues highlighting those tags are required:
Orekit is not able to parse a file generated by itself, so definitely there is something wrong somewhere
The NdmParserTest#testEmpty() tests with the tags. Without the tags the test do not pass and throws the exception.
The standard says: “An NDM combined instantiation shall be delimited with the <ndm></ndm> root element tags instead of one of the individual message tags described in 8.3.2.” My interpretation of this sentence is that they are mandatory. So even if they are no other tags inside.
In that case, how do you know if it’s a simple OPM or a combined NDM with just:
<?xml version="1.0" encoding="UTF-8"?>
In addition, it means that NdmParser#testEmpty do not test properly an empty file and shall be adapted with just the above line (it will throw an exception)
The empty case is not adressed in the CCSDS reference (again, from what i remember ) so i guess we can use an arbitrary solution or simply not accept it and throw an exception (as it is currently implemented if i understand your post). Writing an empty ndm/other file shall then not be possible following the second option’s logic.
For sure, a single OPM shall be delimited with <opm></opm> and a combined NDM instance with <ndm>some other xml tags</ndm>. I completely agree with you on this point.
If no one objects, I’ll update NdmWriter to output the <ndm></ndm> tags even if the file is empty. This way, it will generate the exact same file used by NdmParserTest#testEmpty()