Update JEX Parser method 02/142002/10
authorToineSiebelink <toine.siebelink@est.tech>
Fri, 5 Sep 2025 16:11:39 +0000 (17:11 +0100)
committerToineSiebelink <toine.siebelink@est.tech>
Mon, 15 Sep 2025 16:00:06 +0000 (17:00 +0100)
-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>
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/datajobs/subscription/ncmp/NcmpInEventConsumer.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/utils/JexParser.java
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/datajobs/subscription/ncmp/NcmpInEventConsumerSpec.groovy
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/utils/JexParserSpec.groovy

index 17d6920..141bbd0 100644 (file)
@@ -50,7 +50,7 @@ public class NcmpInEventConsumer {
         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";
index 28b8db9..aa5cbde 100644 (file)
@@ -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<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)
@@ -95,13 +89,25 @@ public class JexParser {
         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;
     }
 }
 
index 6525c32..8290166 100644 (file)
@@ -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'
index 40a1cb8..e8fcfb9 100644 (file)
@@ -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"]'
     }
 }