JEX expression parser 82/141682/11
authorshikha0203 <shivani.khare@est.tech>
Tue, 12 Aug 2025 11:17:37 +0000 (12:17 +0100)
committershikha0203 <shivani.khare@est.tech>
Tue, 19 Aug 2025 14:40:16 +0000 (15:40 +0100)
-Splits a multiline selector string into individual lines
-Resolves the nearest alternate ID from a single selector
-Returns a list of alternate IDs

Issue-ID: CPS-2892
Change-Id: I067c99c81d5ad8da9c582537e7fcf2d8d22e7dc9
Signed-off-by: shikha0203 <shivani.khare@est.tech>
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/utils/JexParser.java [new file with mode: 0644]
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/utils/JexParserSpec.groovy [new file with mode: 0644]

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
new file mode 100644 (file)
index 0000000..28b8db9
--- /dev/null
@@ -0,0 +1,113 @@
+/*
+ *  ============LICENSE_START=======================================================
+ *  Copyright (C) 2025 OpenInfra Foundation Europe. All rights reserved.
+ *  ================================================================================
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ *
+ *  SPDX-License-Identifier: Apache-2.0
+ *  ============LICENSE_END=========================================================
+ */
+
+package org.onap.cps.ncmp.impl.utils;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+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 String JEX_COMMENT_PREFIX = "&&";
+    private static final String LINE_SEPARATOR_REGEX = "\\R";
+    private static final String SEGMENT_SEPARATOR = "/";
+
+    /**
+     * Resolves alternate ids from a JEX basic expression with many paths.
+     *
+     * @param jsonExpression Multi-line JEX string with possible comments and relative xpaths.
+     * @return List of unique alternate ids (FDNs) resolved from each valid path.
+     */
+    public static List<String> extractFdnsFromLocationPaths(final String jsonExpression) {
+        if (jsonExpression == null) {
+            return Collections.emptyList();
+        }
+
+        final String[] lines = jsonExpression.split(LINE_SEPARATOR_REGEX);
+
+        final Stream<String> locationPaths = 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());
+    }
+
+    /**
+     * 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.
+     */
+    private static Optional<String> extractFdnPrefix(final String locationPath) {
+        final List<String> locationPathSegments = splitIntoLocationPathsSegments(locationPath);
+        final StringBuilder fdnBuilder = new StringBuilder();
+        for (final String locationPathSegment : locationPathSegments) {
+
+            final Matcher matcher = LOCATION_SEGMENT_PATTERN.matcher(locationPathSegment);
+            if (matcher.find()) {
+                final String managedObjectName = matcher.group(1);
+                final String managedObjectId = matcher.group(2);
+                fdnBuilder.append(SEGMENT_SEPARATOR)
+                        .append(managedObjectName)
+                        .append("=")
+                        .append(managedObjectId);
+            } else {
+                break;
+            }
+        }
+
+        final String fdn = fdnBuilder.toString();
+        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
+        }
+        return locationPathSegmentsAsList;
+    }
+}
+
+
+
+
+
+
+
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
new file mode 100644 (file)
index 0000000..40a1cb8
--- /dev/null
@@ -0,0 +1,85 @@
+/*
+ *  ============LICENSE_START=======================================================
+ *  Copyright (C) 2025 OpenInfra Foundation Europe. All rights reserved.
+ *  ================================================================================
+ *  Licensed under the Apache License, Version 2.0 (the 'License');
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an 'AS IS' BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ *
+ *  SPDX-License-Identifier: Apache-2.0
+ *  ============LICENSE_END=========================================================
+ */
+
+package org.onap.cps.ncmp.impl.utils
+
+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
+            '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 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)
+        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
+    }
+}
+
+
+
+