From 14b0dc4f39d35b237caffa4e008667846ef9124c Mon Sep 17 00:00:00 2001 From: ToineSiebelink Date: Fri, 5 Sep 2025 17:11:39 +0100 Subject: [PATCH] Update JEX Parser method -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 --- .../subscription/ncmp/NcmpInEventConsumer.java | 2 +- .../org/onap/cps/ncmp/impl/utils/JexParser.java | 76 ++++++++------- .../ncmp/NcmpInEventConsumerSpec.groovy | 2 +- .../onap/cps/ncmp/impl/utils/JexParserSpec.groovy | 104 ++++++++++++--------- 4 files changed, 103 insertions(+), 81 deletions(-) diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/datajobs/subscription/ncmp/NcmpInEventConsumer.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/datajobs/subscription/ncmp/NcmpInEventConsumer.java index 17d6920713..141bbd072c 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/datajobs/subscription/ncmp/NcmpInEventConsumer.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/datajobs/subscription/ncmp/NcmpInEventConsumer.java @@ -50,7 +50,7 @@ public class NcmpInEventConsumer { final String eventType = dataJobSubscriptionOperationInEvent.getEventType(); final String dataNodeSelector = dataJobSubscriptionOperationInEvent.getEvent().getDataJob() .getProductionJobDefinition().getTargetSelector().getDataNodeSelector(); - final List fdns = JexParser.extractFdnsFromLocationPaths(dataNodeSelector); + final List fdns = JexParser.toXpaths(dataNodeSelector); final String dataJobId = dataJobSubscriptionOperationInEvent.getEvent().getDataJob().getId(); final String dataTypeId = dataJobSubscriptionOperationInEvent.getEvent().getDataType() != null ? dataJobSubscriptionOperationInEvent.getEvent().getDataType().getDataTypeId() : "UNKNOWN"; diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/utils/JexParser.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/utils/JexParser.java index 28b8db9f35..aa5cbdee68 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/utils/JexParser.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/utils/JexParser.java @@ -22,64 +22,58 @@ package org.onap.cps.ncmp.impl.utils; 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 extractFdnsFromLocationPaths(final String jsonExpression) { - if (jsonExpression == null) { + @SuppressWarnings("unused") + public static List toXpaths(final String jsonExpressionsAsString) { + if (jsonExpressionsAsString == null) { return Collections.emptyList(); } - - final String[] lines = jsonExpression.split(LINE_SEPARATOR_REGEX); - - final Stream 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 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 extractFdnPrefix(final String locationPath) { - final List locationPathSegments = splitIntoLocationPathsSegments(locationPath); + @SuppressWarnings("unused") + public static Optional extractFdnPrefix(final String xpath) { + final List 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) @@ -95,13 +89,25 @@ public class JexParser { return fdn.isEmpty() ? Optional.empty() : Optional.of(fdn); } - private static List splitIntoLocationPathsSegments(final String locationPath) { - final String[] locationPathSegments = locationPath.split(SEGMENT_SEPARATOR); - final List 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 xpaths) { + return String.join(LINE_JOINER_DELIMITER, xpaths); + } + + private static List splitIntoXpaths(final String xpath) { + final String[] xpathSegments = xpath.split(SEGMENT_SEPARATOR); + final List xpathSegmentsAsList = new ArrayList<>(Arrays.asList(xpathSegments)); + if (!xpathSegmentsAsList.isEmpty()) { + xpathSegmentsAsList.remove(0); // ignore root } - return locationPathSegmentsAsList; + return xpathSegmentsAsList; } } diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/datajobs/subscription/ncmp/NcmpInEventConsumerSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/datajobs/subscription/ncmp/NcmpInEventConsumerSpec.groovy index 6525c326b0..8290166989 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/datajobs/subscription/ncmp/NcmpInEventConsumerSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/datajobs/subscription/ncmp/NcmpInEventConsumerSpec.groovy @@ -67,7 +67,7 @@ class NcmpInEventConsumerSpec extends Specification { 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' diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/utils/JexParserSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/utils/JexParserSpec.groovy index 40a1cb8d05..e8fcfb92b2 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/utils/JexParserSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/utils/JexParserSpec.groovy @@ -24,59 +24,75 @@ import spock.lang.Specification 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"]' } } -- 2.16.6