Commit 4719e590 authored by Luc Maisonobe's avatar Luc Maisonobe
Browse files

Fixed CCSDS tokens filtering for XML files.

Fixes #991
parent 1954df30
Pipeline #2714 passed with stages
in 17 minutes and 20 seconds
......@@ -16,15 +16,16 @@
*/
package org.orekit.files.ccsds.ndm.adm;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.orekit.files.ccsds.utils.lexical.ParseToken;
import org.orekit.files.ccsds.utils.lexical.TokenType;
import org.orekit.files.ccsds.utils.lexical.XmlTokenBuilder;
import org.orekit.utils.units.Unit;
import org.orekit.utils.units.UnitsCache;
import org.xml.sax.Attributes;
/** Builder for rotation angles and rates.
* <p>
......@@ -55,26 +56,29 @@ public class RotationXmlTokenBuilder implements XmlTokenBuilder {
/** {@inheritDoc} */
@Override
public List<ParseToken> buildTokens(final boolean startTag, final String qName,
final String content, final Attributes attributes,
public List<ParseToken> buildTokens(final boolean startTag, final boolean isLeaf, final String qName,
final String content, final Map<String, String> attributes,
final int lineNumber, final String fileName) {
// get the token name from the first attribute found
String name = attributes.getValue(ANGLE);
String name = attributes.get(ANGLE);
if (name == null) {
name = attributes.getValue(RATE);
name = attributes.get(RATE);
}
// elaborate the token type
final TokenType type = (content == null) ? (startTag ? TokenType.START : TokenType.STOP) : TokenType.ENTRY;
// get units
final Unit units = cache.getUnits(attributes.getValue(UNITS));
// final build
final ParseToken token = new ParseToken(type, name, content, units, lineNumber, fileName);
return Collections.singletonList(token);
if (startTag) {
return Collections.singletonList(new ParseToken(TokenType.START, name, content, Unit.NONE, lineNumber, fileName));
} else {
final List<ParseToken> built = new ArrayList<>(2);
if (isLeaf) {
// get units
final Unit units = cache.getUnits(attributes.get(UNITS));
built.add(new ParseToken(TokenType.ENTRY, name, content, units, lineNumber, fileName));
}
built.add(new ParseToken(TokenType.STOP, name, null, Unit.NONE, lineNumber, fileName));
return built;
}
}
......
......@@ -86,7 +86,7 @@ public class KvnLexicalAnalyzer implements LexicalAnalyzer {
/** Regular expression matching non-comment entry with optional units.
* <p>
* Note than since 2.0, we allow empty values at lexical analysis level and detect them at parsing level
* Note than since 12.0, we allow empty values at lexical analysis level and detect them at parsing level
* </p>
*/
private static final Pattern NON_COMMENT_ENTRY = Pattern.compile(LINE_START + NON_COMMENT_KEY + OPTIONAL_VALUE + UNITS + LINE_END);
......
......@@ -19,9 +19,9 @@ package org.orekit.files.ccsds.utils.lexical;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.orekit.utils.units.Unit;
import org.xml.sax.Attributes;
/** Builder for the root element with CCSDS message version.
* <p>
......@@ -53,13 +53,13 @@ public class MessageVersionXmlTokenBuilder implements XmlTokenBuilder {
/** {@inheritDoc} */
@Override
public List<ParseToken> buildTokens(final boolean startTag, final String qName,
final String content, final Attributes attributes,
public List<ParseToken> buildTokens(final boolean startTag, final boolean isLeaf, final String qName,
final String content, final Map<String, String> attributes,
final int lineNumber, final String fileName) {
if (startTag) {
// we replace the start tag with the message version specification
final String id = attributes.getValue(ID);
final String version = attributes.getValue(VERSION);
final String id = attributes.get(ID);
final String version = attributes.get(VERSION);
final ParseToken start = new ParseToken(TokenType.START, qName, null, Unit.NONE, lineNumber, fileName);
final ParseToken entry = new ParseToken(TokenType.ENTRY, id, version, Unit.NONE, lineNumber, fileName);
return Arrays.asList(start, entry);
......
......@@ -16,12 +16,13 @@
*/
package org.orekit.files.ccsds.utils.lexical;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.orekit.utils.units.Unit;
import org.orekit.utils.units.UnitsCache;
import org.xml.sax.Attributes;
/** Regular builder using XML elements names and content for tokens.
* <p>
......@@ -48,22 +49,22 @@ public class RegularXmlTokenBuilder implements XmlTokenBuilder {
/** {@inheritDoc} */
@Override
public List<ParseToken> buildTokens(final boolean startTag, final String qName,
final String content, final Attributes attributes,
public List<ParseToken> buildTokens(final boolean startTag, final boolean isLeaf, final String qName,
final String content, final Map<String, String> attributes,
final int lineNumber, final String fileName) {
// elaborate the token type
final TokenType tokenType = (content == null) ?
(startTag ? TokenType.START : TokenType.STOP) :
TokenType.ENTRY;
// get units
final Unit units = cache.getUnits(attributes.getValue(UNITS));
// final build
final ParseToken token = new ParseToken(tokenType, qName, content, units, lineNumber, fileName);
return Collections.singletonList(token);
if (startTag) {
return Collections.singletonList(new ParseToken(TokenType.START, qName, content, Unit.NONE, lineNumber, fileName));
} else {
final List<ParseToken> built = new ArrayList<>(2);
if (isLeaf) {
// get units
final Unit units = cache.getUnits(attributes.get(UNITS));
built.add(new ParseToken(TokenType.ENTRY, qName, content, units, lineNumber, fileName));
}
built.add(new ParseToken(TokenType.STOP, qName, null, Unit.NONE, lineNumber, fileName));
return built;
}
}
......
......@@ -16,12 +16,13 @@
*/
package org.orekit.files.ccsds.utils.lexical;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.orekit.files.ccsds.ndm.odm.UserDefined;
import org.orekit.utils.units.Unit;
import org.xml.sax.Attributes;
/** Builder for user-defined parameters.
* <p>
......@@ -43,23 +44,24 @@ public class UserDefinedXmlTokenBuilder implements XmlTokenBuilder {
/** {@inheritDoc} */
@Override
public List<ParseToken> buildTokens(final boolean startTag, final String qName,
final String content, final Attributes attributes,
public List<ParseToken> buildTokens(final boolean startTag, final boolean isLeaf, final String qName,
final String content, final Map<String, String> attributes,
final int lineNumber, final String fileName) {
// elaborate the token type
final TokenType tokenType = (content == null) ?
(startTag ? TokenType.START : TokenType.STOP) :
TokenType.ENTRY;
// elaborate name
final String name = UserDefined.USER_DEFINED_PREFIX +
attributes.get(UserDefined.USER_DEFINED_XML_ATTRIBUTE);
// final build
final String name = attributes.getValue(UserDefined.USER_DEFINED_XML_ATTRIBUTE);
final ParseToken token = new ParseToken(tokenType,
UserDefined.USER_DEFINED_PREFIX + name,
content, Unit.NONE,
lineNumber, fileName);
return Collections.singletonList(token);
if (startTag) {
return Collections.singletonList(new ParseToken(TokenType.START, name, content, Unit.NONE, lineNumber, fileName));
} else {
final List<ParseToken> built = new ArrayList<>(2);
if (isLeaf) {
built.add(new ParseToken(TokenType.ENTRY, name, content, Unit.NONE, lineNumber, fileName));
}
built.add(new ParseToken(TokenType.STOP, name, null, Unit.NONE, lineNumber, fileName));
return built;
}
}
......
......@@ -19,6 +19,8 @@ package org.orekit.files.ccsds.utils.lexical;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import javax.xml.parsers.ParserConfigurationException;
......@@ -120,7 +122,17 @@ public class XmlLexicalAnalyzer implements LexicalAnalyzer {
private String currentContent;
/** Attributes of the current element. */
private Attributes currentAttributes;
private Map<String, String> currentAttributes;
/** Last processed token qualified name.
* @since 12.0
*/
private String lastQname;
/** Last processed token start/end indicator.
* @since 12.0
*/
private boolean lastWasStart;
/** Simple constructor.
* @param messageParser CCSDS Message parser to use
......@@ -129,6 +141,8 @@ public class XmlLexicalAnalyzer implements LexicalAnalyzer {
this.messageParser = messageParser;
this.regularBuilder = new RegularXmlTokenBuilder();
this.specialElements = messageParser.getSpecialXmlElementsBuilders();
this.lastQname = "";
this.lastWasStart = false;
}
/** Get a builder for the current element.
......@@ -173,15 +187,26 @@ public class XmlLexicalAnalyzer implements LexicalAnalyzer {
public void startElement(final String uri, final String localName, final String qName, final Attributes attributes) {
currentElementName = qName;
currentAttributes = attributes;
currentLineNumber = locator.getLineNumber();
currentContent = null;
// save attributes in separate map, to avoid overriding during parsing
if (attributes.getLength() == 0) {
currentAttributes = Collections.emptyMap();
} else {
currentAttributes = new HashMap<>(attributes.getLength());
for (int i = 0; i < attributes.getLength(); ++i) {
currentAttributes.put(attributes.getQName(i), attributes.getValue(i));
}
}
for (final ParseToken token : getBuilder(qName).
buildTokens(true, qName, currentContent, currentAttributes,
buildTokens(true, false, qName, currentContent, currentAttributes,
currentLineNumber, source.getName())) {
messageParser.process(token);
}
lastQname = qName;
lastWasStart = true;
}
......@@ -194,13 +219,19 @@ public class XmlLexicalAnalyzer implements LexicalAnalyzer {
currentLineNumber = locator.getLineNumber();
}
// check if we are parsing the end tag of a leaf element
final boolean isLeaf = lastWasStart && qName.equals(lastQname);
for (final ParseToken token : getBuilder(qName).
buildTokens(false, qName, currentContent, currentAttributes,
buildTokens(false, isLeaf, qName, currentContent, currentAttributes,
currentLineNumber, source.getName())) {
messageParser.process(token);
}
lastQname = qName;
lastWasStart = true;
currentElementName = null;
currentAttributes = null;
currentLineNumber = -1;
currentContent = null;
......
......@@ -17,8 +17,7 @@
package org.orekit.files.ccsds.utils.lexical;
import java.util.List;
import org.xml.sax.Attributes;
import java.util.Map;
/** Builder for building {@link ParseToken} from XML elements.
* <p>
......@@ -39,15 +38,17 @@ public interface XmlTokenBuilder {
/** Create a list of parse tokens.
* @param startTag if true we are parsing the start tag from an XML element
* @param isLeaf if true and startTag is false, we are processing the end tag of a leaf XML element
* @param qName element qualified name
* @param content element content
* @param attributes element attributes
* @param lineNumber number of the line in the CCSDS data message
* @param fileName name of the file
* @return list of parse tokens
* @since 12.0
*/
List<ParseToken> buildTokens(boolean startTag, String qName,
String content, Attributes attributes,
List<ParseToken> buildTokens(boolean startTag, boolean isLeaf, String qName,
String content, Map<String, String> attributes,
int lineNumber, String fileName);
}
......@@ -211,7 +211,7 @@ public abstract class AbstractMessageParser<T> implements MessageParser<T> {
if (filteredToken.getType() == TokenType.ENTRY &&
!COMMENT.equals(filteredToken.getName()) &&
filteredToken.getRawContent().isEmpty()) {
(filteredToken.getRawContent() == null || filteredToken.getRawContent().isEmpty())) {
// value is empty, which is forbidden by CCSDS standards
throw new OrekitException(OrekitMessages.UNINITIALIZED_VALUE_FOR_KEY, filteredToken.getName());
}
......
......@@ -41,7 +41,7 @@ one segment whereas `Oem` may contain several segments).
There are as many sub-packages as there are CCSDS message types, with
intermediate sub-packages for each officially published recommendation:
`org.orekit.files.ccsds.ndm.adm.apm`, `org.orekit.files.ccsds.ndm.adm.aem`,
`org.orekit.files.ccsds.ndm.cdm.`, `org.orekit.files.ccsds.ndm.odm.opm`,
`org.orekit.files.ccsds.ndm.cdm`, `org.orekit.files.ccsds.ndm.odm.opm`,
`org.orekit.files.ccsds.ndm.odm.oem`, `org.orekit.files.ccsds.ndm.odm.omm`,
`org.orekit.files.ccsds.ndm.odm.ocm`, and `org.orekit.files.ccsds.ndm.tdm`.
Each contain the logical structures
......@@ -162,7 +162,7 @@ the parsers. There are several use cases for this feature.
the development of this feature) is OMM files in XML format that had an empty `OBJECT_ID`,
which is forbidden by CCSDS standard. These non compliant messages could be fixed by
setting a filter that recognizes `OBJECT_ID` entries with empty value and replace them
with a value set to `unknown' before passing the changed token back to the parser
with a value set to `unknown` before passing the changed token back to the parser
2) remove unwanted data, for example removing all user-defined data is done by setting
a filter that returns an empty list of tokens when presented with a user-defined entry
3) add data not originally present in the file. For example one could add generated ODM
......
......@@ -344,7 +344,7 @@ public class OmmParserTest {
withFilter(token -> {
if ("OBJECT_ID".equals(token.getName()) &&
(token.getRawContent() == null || token.getRawContent().isEmpty())) {
// replace null/empty entries with "unknown"
// replace null/empty entries with specified value
return Collections.singletonList(new ParseToken(token.getType(), token.getName(),
replacement, token.getUnits(),
token.getLineNumber(), token.getFileName()));
......@@ -359,6 +359,47 @@ public class OmmParserTest {
}
@Test
public void testEmptyObjectIDXml() throws URISyntaxException {
// test with an OMM file that does not fulfills CCSDS standard and uses an empty OBJECT_ID
String name = "/ccsds/odm/omm/OMM-empty-object-id.xml";
final DataSource source = new DataSource(name, () -> getClass().getResourceAsStream(name));
final OmmParser parser = new ParserBuilder().
withMu(Constants.EIGEN5C_EARTH_MU).
withMissionReferenceDate(new AbsoluteDate()).
withDefaultMass(1000.0).
buildOmmParser();
try {
parser.parseMessage(source);
Assertions.fail("an exception should have been thrown");
} catch (OrekitException oe) {
oe.printStackTrace();
Assertions.assertEquals(OrekitMessages.UNINITIALIZED_VALUE_FOR_KEY, oe.getSpecifier());
Assertions.assertEquals("OBJECT_ID", oe.getParts()[0]);
}
final String replacement = "replacement-object-id";
final Omm omm = new ParserBuilder().
withMu(Constants.EIGEN5C_EARTH_MU).
withMissionReferenceDate(new AbsoluteDate()).
withDefaultMass(1000.0).
withFilter(token -> {
if ("OBJECT_ID".equals(token.getName()) &&
(token.getRawContent() == null || token.getRawContent().isEmpty())) {
// replace null/empty entries with specified value
return Collections.singletonList(new ParseToken(token.getType(), token.getName(),
replacement, token.getUnits(),
token.getLineNumber(), token.getFileName()));
} else {
return Collections.singletonList(token);
}
}).
buildOmmParser().
parseMessage(source);
// note that object id is always converted to uppercase during parsing
Assertions.assertEquals(replacement.toUpperCase(), omm.getMetadata().getObjectID());
}
@Test
public void testRemoveUserData() throws URISyntaxException {
final String name = "/ccsds/odm/omm/OMMExample3.txt";
......
<?xml version="1.0" encoding="UTF-8"?>
<omm id="CCSDS_OMM_VERS" version="2.0">
<header>
<CREATION_DATE>2021-03-24T23:00:00.000</CREATION_DATE>
<ORIGINATOR>CelesTrak</ORIGINATOR>
</header>
<body>
<segment>
<metadata>
<OBJECT_NAME>STARLETTE</OBJECT_NAME>
<OBJECT_ID></OBJECT_ID>
<CENTER_NAME>EARTH</CENTER_NAME>
<REF_FRAME>TEME</REF_FRAME>
<TIME_SYSTEM>UTC</TIME_SYSTEM>
<MEAN_ELEMENT_THEORY>SGP4</MEAN_ELEMENT_THEORY>
</metadata>
<data>
<meanElements>
<EPOCH>2021-03-22T13:21:09.224928</EPOCH>
<MEAN_MOTION>13.82309053</MEAN_MOTION>
<ECCENTRICITY>.0205751</ECCENTRICITY>
<INCLINATION>49.8237</INCLINATION>
<RA_OF_ASC_NODE>93.8140</RA_OF_ASC_NODE>
<ARG_OF_PERICENTER>224.8348</ARG_OF_PERICENTER>
<MEAN_ANOMALY>133.5761</MEAN_ANOMALY>
</meanElements>
<tleParameters>
<EPHEMERIS_TYPE>0</EPHEMERIS_TYPE>
<CLASSIFICATION_TYPE>U</CLASSIFICATION_TYPE>
<NORAD_CAT_ID>7646</NORAD_CAT_ID>
<ELEMENT_SET_NO>999</ELEMENT_SET_NO>
<REV_AT_EPOCH>32997</REV_AT_EPOCH>
<BSTAR>-.47102E-5</BSTAR>
<MEAN_MOTION_DOT>-.147E-5</MEAN_MOTION_DOT>
<MEAN_MOTION_DDOT>0</MEAN_MOTION_DDOT>
</tleParameters>
</data>
</segment>
</body>
</omm>
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment