-JEX parser needs to be updated to accommodate consumer and service methods in NCMP
-updated NcmpInEventConsumer and NcmpInEventConsumerSpec class
-Ensure REGEX for XPaths is safe and performant
Issue-ID: CPS-2976
Change-Id: Ibe55c2574d49561f989463702f4f8a495d9de35f
Signed-off-by: ToineSiebelink <toine.siebelink@est.tech>
final String eventType = dataJobSubscriptionOperationInEvent.getEventType();
final String dataNodeSelector = dataJobSubscriptionOperationInEvent.getEvent().getDataJob()
.getProductionJobDefinition().getTargetSelector().getDataNodeSelector();
- final List<String> fdns = JexParser.extractFdnsFromLocationPaths(dataNodeSelector);
+ final List<String> fdns = JexParser.toXpaths(dataNodeSelector);
final String dataJobId = dataJobSubscriptionOperationInEvent.getEvent().getDataJob().getId();
final String dataTypeId = dataJobSubscriptionOperationInEvent.getEvent().getDataType() != null
? dataJobSubscriptionOperationInEvent.getEvent().getDataType().getDataTypeId() : "UNKNOWN";
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class JexParser {
- private static final Pattern LOCATION_SEGMENT_PATTERN = Pattern.compile("(.*)\\[id=\\\"(.*)\\\"]");
+ private static final Pattern XPATH_SEGMENT_PATTERN = Pattern.compile("^([^]]*)\\[id=\\\"([^\\\"]*)\\\"]");
private static final String JEX_COMMENT_PREFIX = "&&";
private static final String LINE_SEPARATOR_REGEX = "\\R";
+ private static final String LINE_JOINER_DELIMITER = "\n";
private static final String SEGMENT_SEPARATOR = "/";
/**
- * Resolves alternate ids from a JEX basic expression with many paths.
+ * This method will remove duplicates, blank lines and jex comments.
*
- * @param jsonExpression Multi-line JEX string with possible comments and relative xpaths.
- * @return List of unique alternate ids (FDNs) resolved from each valid path.
+ * @param jsonExpressionsAsString Multi-line jex string.
+ * @return List of xpaths
*/
- public static List<String> extractFdnsFromLocationPaths(final String jsonExpression) {
- if (jsonExpression == null) {
+ @SuppressWarnings("unused")
+ public static List<String> toXpaths(final String jsonExpressionsAsString) {
+ if (jsonExpressionsAsString == null) {
return Collections.emptyList();
}
-
- final String[] lines = jsonExpression.split(LINE_SEPARATOR_REGEX);
-
- final Stream<String> locationPaths = Arrays.stream(lines)
+ final String[] lines = jsonExpressionsAsString.split(LINE_SEPARATOR_REGEX);
+ return Arrays.stream(lines)
.map(String::trim)
- .filter(locationPath -> !locationPath.startsWith(JEX_COMMENT_PREFIX));
-
- final Stream<String> fdns = locationPaths
- .map(JexParser::extractFdnPrefix)
- .flatMap(Optional::stream)
- .distinct();
-
- return fdns.collect(Collectors.toList());
+ .filter(xpath -> !xpath.startsWith(JEX_COMMENT_PREFIX))
+ .distinct()
+ .toList();
}
/**
- * Returns FDN from a JSON expression as a java Optional.
+ * Returns fdn from a json expression as a java Optional.
* Example: /SubNetwork[id="SN1"]/ManagedElement[id="ME1"]
* returns: /SubNetwork=SN1/ManagedElement=ME1
*
- * @param locationPath A single JEX path.
- * @return Optional containing resolved FDN if found; empty otherwise.
+ * @param xpath A single json expression.
+ * @return Optional containing resolved fdn if found; empty otherwise.
*/
- private static Optional<String> extractFdnPrefix(final String locationPath) {
- final List<String> locationPathSegments = splitIntoLocationPathsSegments(locationPath);
+ @SuppressWarnings("unused")
+ public static Optional<String> extractFdnPrefix(final String xpath) {
+ final List<String> xpathSegments = splitIntoXpaths(xpath);
final StringBuilder fdnBuilder = new StringBuilder();
- for (final String locationPathSegment : locationPathSegments) {
-
- final Matcher matcher = LOCATION_SEGMENT_PATTERN.matcher(locationPathSegment);
- if (matcher.find()) {
+ for (final String xpathSegment : xpathSegments) {
+ final Matcher matcher = XPATH_SEGMENT_PATTERN.matcher(xpathSegment);
+ if (matcher.matches()) {
final String managedObjectName = matcher.group(1);
final String managedObjectId = matcher.group(2);
fdnBuilder.append(SEGMENT_SEPARATOR)
return fdn.isEmpty() ? Optional.empty() : Optional.of(fdn);
}
- private static List<String> splitIntoLocationPathsSegments(final String locationPath) {
- final String[] locationPathSegments = locationPath.split(SEGMENT_SEPARATOR);
- final List<String> locationPathSegmentsAsList = new ArrayList<>(Arrays.asList(locationPathSegments));
- if (!locationPathSegmentsAsList.isEmpty()) {
- locationPathSegmentsAsList.remove(0); // ignore root
+ /**
+ * Concatenates the given list of xpaths into a single json expression string.
+ * Each path separated by the {@code LINE_JOINER_DELIMITER}.
+ *
+ * @param xpaths List of xpath strings to be joined.
+ * @return A string representing the concatenated json expression.
+ */
+ @SuppressWarnings("unused")
+ public static String toJsonExpressionsAsString(final Collection<String> xpaths) {
+ return String.join(LINE_JOINER_DELIMITER, xpaths);
+ }
+
+ private static List<String> splitIntoXpaths(final String xpath) {
+ final String[] xpathSegments = xpath.split(SEGMENT_SEPARATOR);
+ final List<String> xpathSegmentsAsList = new ArrayList<>(Arrays.asList(xpathSegments));
+ if (!xpathSegmentsAsList.isEmpty()) {
+ xpathSegmentsAsList.remove(0); // ignore root
}
- return locationPathSegmentsAsList;
+ return xpathSegmentsAsList;
}
}
assert loggingEvent.formattedMessage.contains('jobId=my job id')
assert loggingEvent.formattedMessage.contains('eventType=my event type')
assert loggingEvent.formattedMessage.contains("dataType=${dataTypeId}")
- assert loggingEvent.formattedMessage.contains('fdns=[/SubNetwork=SN1]')
+ assert loggingEvent.formattedMessage.contains('fdns=[/SubNetwork[id="SN1"]]')
where: 'the following data type ids are used'
scenario | dataTypeId
'with' | 'my data type'
class JexParserSpec extends Specification {
- def 'Parsing single JSON Expression with #scenario.'() {
- when: 'the parser extracts FDNs'
- def result = JexParser.extractFdnsFromLocationPaths(locationPath)
- then: 'only the expected top-level absolute paths with id is returned'
- assert result[0] == expectedFdn
- where: 'Following expressions are used'
- scenario | locationPath || expectedFdn
+ def 'Parsing multi-line json expressions with #scenario.'() {
+ when: 'the parser gets a (multi-line) json expressions'
+ def result = JexParser.toXpaths(jsonExpressions)
+ then: 'the expected xpaths are returned'
+ assert result == ['/SubNetwork[id="SN1"]']
+ where: 'following expressions are used'
+ scenario | jsonExpressions
+ 'single xpath' | '/SubNetwork[id="SN1"]'
+ 'xpath with spaces' | ' /SubNetwork[id="SN1"] '
+ 'duplicate xpaths' | '/SubNetwork[id="SN1"]\n/SubNetwork[id="SN1"]'
+ 'preceding commented line' | '&&ignore this\n/SubNetwork[id="SN1"]'
+ }
+
+ def 'Parsing multi-line json expressions with multiple xpaths.'() {
+ given: 'multi-line json expressions'
+ def jsonExpressions = '/SubNetwork[id="SN1"]\n/ManagedElement[id="ME1"]'
+ when: 'convert it to xpaths'
+ def result = JexParser.toXpaths(jsonExpressions)
+ then: 'the expected xpaths are returned'
+ assert result == ['/SubNetwork[id="SN1"]','/ManagedElement[id="ME1"]']
+ }
+
+ def 'Extracts xpaths from json expressions, ignored expressions: #scenario.'() {
+ when: 'the parser gets a json expressions with #scenario'
+ def result = JexParser.toXpaths(jsonExpressions)
+ then: 'the result is empty'
+ assert result.isEmpty()
+ where: 'following expressions are used'
+ scenario | jsonExpressions
+ 'null input' | null
+ 'comments only' | '&&text only comment'
+ 'commented out FDN' | '&&/SubNetwork[id="SN1"]/ManagedElement[id="ME1"]'
+ }
+
+ def 'Convert xpaths to json expressions.'() {
+ given: 'list of xpaths'
+ def xpaths = ['/SubNetwork[id="SN1"]', '/ManagedElement']
+ when: 'converting the xpaths into a json expression'
+ def result = JexParser.toJsonExpressionsAsString(xpaths)
+ then: 'the expected multi-line json expression returned'
+ assert result == '/SubNetwork[id="SN1"]\n/ManagedElement'
+ }
+
+ def 'Extracts fdn from xpath with #scenario.'() {
+ when: 'the parser extracts the fdn (prefix)'
+ def result = JexParser.extractFdnPrefix(xpath)
+ then: 'the expected FDN is returned'
+ assert result.get() == expectedFdn
+ where: 'Following xpaths are used'
+ scenario | xpath || expectedFdn
'single segment' | '/SubNetwork[id="SN1"]' || '/SubNetwork=SN1'
'two segments' | '/SubNetwork[id="SN1"]/ManagedElement[id="ME1"]' || '/SubNetwork=SN1/ManagedElement=ME1'
- 'segment and mo without id' | '/SubNetwork[id="SN1"]/attributes]' || '/SubNetwork=SN1'
+ 'segment and mo without id' | '/SubNetwork[id="SN1"]/attributes' || '/SubNetwork=SN1'
'segment and mos without id' | '/SubNetwork[id="SN1"]/attributes/vendorName' || '/SubNetwork=SN1'
'segment and mo with other attribute expressions' | '/SubNetwork[id="SN1"]/vendor[name="V1"]' || '/SubNetwork=SN1'
'segment followed by wildcard' | '/SubNetwork[id="SN1"]/*' || '/SubNetwork=SN1'
}
- def 'Parsing multiple JSON Expressions.'() {
- given: 'multiple JSON expressions with multiple absolute paths, attributes, and filters'
- def locationPath = """
- /SubNetwork[id="SN1"]/ManagedElement
- /SubNetwork[id="SN2"]
- """
- when: 'the parser extracts FDNs'
- def result = JexParser.extractFdnsFromLocationPaths(locationPath)
- then: 'the expected paths with ids are returned'
- assert result.size() == 2
- assert result.containsAll(['/SubNetwork=SN1', '/SubNetwork=SN2'])
- }
-
- def 'Parsing multiple JSON Expressions with duplicate results.'() {
- given: 'multiple JSON expressions with multiple absolute paths, attributes, and filters'
- def locationPath = """
- /SubNetwork[id="SN1"]/ManagedElement
- /SubNetwork[id="SN1"]
- """
- when: 'the parser extracts FDNs'
- def result = JexParser.extractFdnsFromLocationPaths(locationPath)
- then: 'only one unique path with id is returned'
- assert result == ['/SubNetwork=SN1']
- }
-
- def 'Ignored expressions #scenario.'() {
- when: 'the parser extracts FDNs'
- def result = JexParser.extractFdnsFromLocationPaths(locationPath)
+ def 'Extracts fdn from xpath, ignored expressions: #scenario.'() {
+ when: 'the parser attempt to extracts fdns'
+ def result = JexParser.extractFdnPrefix(xpaths)
then: 'the result is empty'
assert result.isEmpty()
- where: 'Following expressions are used'
- scenario | locationPath
- 'comments' | '&&text only comment'
- 'commented out FDN' | '&&/SubNetwork[id="SN1"]/ManagedElement[id="ME1"]'
- 'blank' | ''
- 'root' | '/'
- 'no IDs at all' | '/SubNetwork/attribute'
- 'null' | null
+ where: 'Following xpaths are used'
+ scenario | xpaths
+ 'blank' | ''
+ 'root' | '/'
+ 'Segments without IDs' | '/SubNetwork/attributes'
+ 'First segment without ID' | '/SubNetwork/ManagedElement[id="1"]'
}
}