From: shikha0203 Date: Tue, 12 Aug 2025 11:17:37 +0000 (+0100) Subject: JEX expression parser X-Git-Url: https://gerrit.onap.org/r/gitweb?a=commitdiff_plain;h=9cc3dcf7d952ff1558d33276d3fbd82345d140f0;p=cps.git JEX expression parser -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 --- 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 index 0000000000..28b8db9f35 --- /dev/null +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/utils/JexParser.java @@ -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 extractFdnsFromLocationPaths(final String jsonExpression) { + if (jsonExpression == null) { + return Collections.emptyList(); + } + + final String[] lines = jsonExpression.split(LINE_SEPARATOR_REGEX); + + final Stream locationPaths = 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()); + } + + /** + * 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 extractFdnPrefix(final String locationPath) { + final List 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 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 + } + 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 index 0000000000..40a1cb8d05 --- /dev/null +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/utils/JexParserSpec.groovy @@ -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 + } +} + + + +