From f0527c58c17963d940535d0ce0eb934c2b4c635c Mon Sep 17 00:00:00 2001 From: ToineSiebelink Date: Tue, 6 Jul 2021 13:03:03 +0100 Subject: Support text() condition - Added Antlr parsing of text() condition (as an optional additional to any query) - Implemented text-condition combined with descendants - Refactor descendants queries into using one more flexible Custom (native) Query builder - Refactor ALL cpsPath queries to now use FragmentRepositoryCpsPathQuery (custom query builder) - Refactor Antrl code to simply parsing of cpsPath and allow all combinations (no more query types, addresses CPS-436) - Minor clean up of some minor convention issues in CpsAdminServiceImplSpec.groovy (found during groovy demo) - Update .rst documentation of xPaths - Fixed incorrect matching of additional list indexes using more precise SIMILAR-TO regex in postgreSQL - Documented special chararter limitation (CPS-500) - Checked for consistent use of term 'CPS path' in documentation and error message - Included (updated) copyright in all .SQL test files Issue-ID: CPS-452 Issue-ID: CPS-436 Issue-ID: CPS-500 Signed-off-by: ToineSiebelink Change-Id: If422d25cafd2850d25c9a28dea16ba7a5f93dddb --- .../spi/impl/CpsDataPersistenceServiceImpl.java | 17 +-- .../cps/spi/repository/FragmentRepository.java | 41 ++----- .../repository/FragmentRepositoryCpsPathQuery.java | 29 +++++ .../FragmentRepositoryCpsPathQueryImpl.java | 120 +++++++++++++++++++++ 4 files changed, 161 insertions(+), 46 deletions(-) create mode 100644 cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentRepositoryCpsPathQuery.java create mode 100644 cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentRepositoryCpsPathQueryImpl.java (limited to 'cps-ri/src/main') diff --git a/cps-ri/src/main/java/org/onap/cps/spi/impl/CpsDataPersistenceServiceImpl.java b/cps-ri/src/main/java/org/onap/cps/spi/impl/CpsDataPersistenceServiceImpl.java index 844ad8474..6e12d0601 100644 --- a/cps-ri/src/main/java/org/onap/cps/spi/impl/CpsDataPersistenceServiceImpl.java +++ b/cps-ri/src/main/java/org/onap/cps/spi/impl/CpsDataPersistenceServiceImpl.java @@ -38,7 +38,6 @@ import java.util.regex.Pattern; import java.util.stream.Collectors; import javax.transaction.Transactional; import org.onap.cps.cpspath.parser.CpsPathQuery; -import org.onap.cps.cpspath.parser.CpsPathQueryType; import org.onap.cps.spi.CpsDataPersistenceService; import org.onap.cps.spi.FetchDescendantsOption; import org.onap.cps.spi.entities.AnchorEntity; @@ -179,20 +178,8 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService } catch (final IllegalStateException e) { throw new CpsPathException(e.getMessage()); } - List fragmentEntities; - if (CpsPathQueryType.XPATH_LEAF_VALUE.equals(cpsPathQuery.getCpsPathQueryType())) { - fragmentEntities = fragmentRepository - .getByAnchorAndXpathAndLeafAttributes(anchorEntity.getId(), cpsPathQuery.getXpathPrefix(), - cpsPathQuery.getLeafName(), cpsPathQuery.getLeafValue()); - } else if (CpsPathQueryType.XPATH_HAS_DESCENDANT_WITH_LEAF_VALUES.equals(cpsPathQuery.getCpsPathQueryType())) { - final String leafDataAsJson = GSON.toJson(cpsPathQuery.getLeavesData()); - fragmentEntities = fragmentRepository - .getByAnchorAndDescendentNameAndLeafValues(anchorEntity.getId(), cpsPathQuery.getDescendantName(), - leafDataAsJson); - } else { - fragmentEntities = fragmentRepository - .getByAnchorAndXpathEndsInDescendantName(anchorEntity.getId(), cpsPathQuery.getDescendantName()); - } + List fragmentEntities = + fragmentRepository.findByAnchorAndCpsPath(anchorEntity.getId(), cpsPathQuery); if (cpsPathQuery.hasAncestorAxis()) { final Set ancestorXpaths = processAncestorXpath(fragmentEntities, cpsPathQuery); fragmentEntities = ancestorXpaths.isEmpty() diff --git a/cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentRepository.java b/cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentRepository.java index 4d7e7ff54..c48c79ef6 100755 --- a/cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentRepository.java +++ b/cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentRepository.java @@ -1,6 +1,6 @@ /* * ============LICENSE_START======================================================= - * Copyright (C) 2020-201 Nordix Foundation. + * Copyright (C) 2020-2021 Nordix Foundation. * Modifications Copyright (C) 2020-2021 Bell Canada. * Modifications Copyright (C) 2020-2021 Pantheon.tech. * ================================================================================ @@ -38,13 +38,15 @@ import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; @Repository -public interface FragmentRepository extends JpaRepository { +public interface FragmentRepository extends JpaRepository, FragmentRepositoryCpsPathQuery { Optional findByDataspaceAndAnchorAndXpath(@NonNull DataspaceEntity dataspaceEntity, - @NonNull AnchorEntity anchorEntity, @NonNull String xpath); + @NonNull AnchorEntity anchorEntity, + @NonNull String xpath); default FragmentEntity getByDataspaceAndAnchorAndXpath(@NonNull DataspaceEntity dataspaceEntity, - @NonNull AnchorEntity anchorEntity, @NonNull String xpath) { + @NonNull AnchorEntity anchorEntity, + @NonNull String xpath) { return findByDataspaceAndAnchorAndXpath(dataspaceEntity, anchorEntity, xpath) .orElseThrow(() -> new DataNodeNotFoundException(dataspaceEntity.getName(), anchorEntity.getName(), xpath)); } @@ -52,42 +54,19 @@ public interface FragmentRepository extends JpaRepository @Query( value = "SELECT * FROM FRAGMENT WHERE anchor_id = :anchor AND dataspace_id = :dataspace AND parent_id is NULL", nativeQuery = true) - List findRootsByDataspaceAndAnchor( - @Param("dataspace") int dataspaceId, @Param("anchor") int anchorId); + List findRootsByDataspaceAndAnchor(@Param("dataspace") int dataspaceId, + @Param("anchor") int anchorId); default FragmentEntity findFirstRootByDataspaceAndAnchor(@NonNull DataspaceEntity dataspaceEntity, - @NonNull AnchorEntity anchorEntity) { + @NonNull AnchorEntity anchorEntity) { return findRootsByDataspaceAndAnchor(dataspaceEntity.getId(), anchorEntity.getId()).stream().findFirst() .orElseThrow(() -> new DataNodeNotFoundException(dataspaceEntity.getName(), anchorEntity.getName())); } List findAllByAnchorAndXpathIn(@NonNull AnchorEntity anchorEntity, - @NonNull Collection xpath); + @NonNull Collection xpath); @Modifying @Query("DELETE FROM FragmentEntity fe WHERE fe.anchor IN (:anchors)") void deleteByAnchorIn(@NotNull @Param("anchors") Collection anchorEntities); - - @Query(value = - "SELECT * FROM FRAGMENT WHERE (anchor_id = :anchor) AND (xpath = (:xpath) OR xpath LIKE " - + "CONCAT(:xpath,'\\[@%]')) AND attributes @> jsonb_build_object(:leafName , :leafValue)", - nativeQuery = true) - // Above query will match an xpath with or without the index for a list [@key=value] and match anchor id, - // leaf name and leaf value - List getByAnchorAndXpathAndLeafAttributes(@Param("anchor") int anchorId, @Param("xpath") - String xpathPrefix, @Param("leafName") String leafName, @Param("leafValue") Object leafValue); - - @Query(value = "SELECT * FROM FRAGMENT WHERE anchor_id = :anchor AND xpath LIKE CONCAT('%/',:descendantName)", - nativeQuery = true) - // Above query will match the anchor id and last descendant name - List getByAnchorAndXpathEndsInDescendantName(@Param("anchor") int anchorId, - @Param("descendantName") String descendantName); - - @Query(value = "SELECT * FROM FRAGMENT WHERE anchor_id = :anchor AND (xpath LIKE CONCAT('%/',:descendantName) OR " - + "xpath LIKE CONCAT('%/', :descendantName,'\\[@%]')) AND attributes @> :leafDataAsJson\\:\\:jsonb", - nativeQuery = true) - // Above query will match the anchor id, last descendant name and all parameters passed into leafDataASJson with the - // attribute values of the requested data node eg: {"leaf_name":"value", "another_leaf_name":"another value"}​​​​​​ - List getByAnchorAndDescendentNameAndLeafValues(@Param("anchor") int anchorId, - @Param("descendantName") String descendantName, @Param("leafDataAsJson") String leafDataAsJson); } diff --git a/cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentRepositoryCpsPathQuery.java b/cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentRepositoryCpsPathQuery.java new file mode 100644 index 000000000..04138ecc6 --- /dev/null +++ b/cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentRepositoryCpsPathQuery.java @@ -0,0 +1,29 @@ +/*- + * ============LICENSE_START======================================================= + * Copyright (C) 2021 Nordix Foundation. + * ================================================================================ + * 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.spi.repository; + +import java.util.List; +import org.onap.cps.cpspath.parser.CpsPathQuery; +import org.onap.cps.spi.entities.FragmentEntity; + +public interface FragmentRepositoryCpsPathQuery { + List findByAnchorAndCpsPath(int anchorId, CpsPathQuery cpsPathQuery); +} diff --git a/cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentRepositoryCpsPathQueryImpl.java b/cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentRepositoryCpsPathQueryImpl.java new file mode 100644 index 000000000..4aa3e5fb3 --- /dev/null +++ b/cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentRepositoryCpsPathQueryImpl.java @@ -0,0 +1,120 @@ +/*- + * ============LICENSE_START======================================================= + * Copyright (C) 2021 Nordix Foundation. + * ================================================================================ + * 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.spi.repository; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import javax.persistence.Query; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.onap.cps.cpspath.parser.CpsPathPrefixType; +import org.onap.cps.cpspath.parser.CpsPathQuery; +import org.onap.cps.spi.entities.FragmentEntity; + +public class FragmentRepositoryCpsPathQueryImpl implements FragmentRepositoryCpsPathQuery { + + public static final String SIMILAR_TO_ABSOLUTE_PATH_PREFIX = "%/"; + public static final String SIMILAR_TO_OPTIONAL_LIST_INDEX_POSTFIX = "(\\[[^/]*])?"; + + @PersistenceContext + private EntityManager entityManager; + + private static final Gson GSON = new GsonBuilder().create(); + + @Override + public List findByAnchorAndCpsPath(final int anchorId, final CpsPathQuery cpsPathQuery) { + final var sqlStringBuilder = new StringBuilder("SELECT * FROM FRAGMENT WHERE anchor_id = :anchorId"); + final Map queryParameters = new HashMap<>(); + queryParameters.put("anchorId", anchorId); + sqlStringBuilder.append(" AND xpath SIMILAR TO :xpathRegex"); + final String xpathRegex = getSimilarToXpathSqlRegex(cpsPathQuery); + queryParameters.put("xpathRegex", xpathRegex); + if (cpsPathQuery.hasLeafConditions()) { + sqlStringBuilder.append(" AND attributes @> :leafDataAsJson\\:\\:jsonb"); + queryParameters.put("leafDataAsJson", GSON.toJson(cpsPathQuery.getLeavesData())); + } + + addTextFunctionCondition(cpsPathQuery, sqlStringBuilder, queryParameters); + final var query = entityManager.createNativeQuery(sqlStringBuilder.toString(), FragmentEntity.class); + setQueryParameters(query, queryParameters); + return query.getResultList(); + } + + @NotNull + private static String getSimilarToXpathSqlRegex(final CpsPathQuery cpsPathQuery) { + final var xpathRegexBuilder = new StringBuilder(); + if (CpsPathPrefixType.ABSOLUTE.equals(cpsPathQuery.getCpsPathPrefixType())) { + xpathRegexBuilder.append(escapeXpath(cpsPathQuery.getXpathPrefix())); + } else { + xpathRegexBuilder.append(SIMILAR_TO_ABSOLUTE_PATH_PREFIX); + xpathRegexBuilder.append(escapeXpath(cpsPathQuery.getDescendantName())); + } + xpathRegexBuilder.append(SIMILAR_TO_OPTIONAL_LIST_INDEX_POSTFIX); + return xpathRegexBuilder.toString(); + } + + @NotNull + private static String escapeXpath(final String xpath) { + // See https://jira.onap.org/browse/CPS-500 for limitations of this basic escape mechanism + return xpath.replace("[@", "\\[@"); + } + + @Nullable + private static Integer getTextValueAsInt(final CpsPathQuery cpsPathQuery) { + try { + return Integer.parseInt(cpsPathQuery.getTextFunctionConditionValue()); + } catch (final NumberFormatException e) { + return null; + } + } + + private static void addTextFunctionCondition(final CpsPathQuery cpsPathQuery, final StringBuilder sqlStringBuilder, + final Map queryParameters) { + if (cpsPathQuery.hasTextFunctionCondition()) { + sqlStringBuilder.append(" AND ("); + sqlStringBuilder.append("attributes @> jsonb_build_object(:textLeafName, :textValue)"); + sqlStringBuilder + .append(" OR attributes @> jsonb_build_object(:textLeafName, json_build_array(:textValue))"); + queryParameters.put("textLeafName", cpsPathQuery.getTextFunctionConditionLeafName()); + queryParameters.put("textValue", cpsPathQuery.getTextFunctionConditionValue()); + final var textValueAsInt = getTextValueAsInt(cpsPathQuery); + if (textValueAsInt != null) { + sqlStringBuilder.append(" OR attributes @> jsonb_build_object(:textLeafName, :textValueAsInt)"); + sqlStringBuilder + .append(" OR attributes @> jsonb_build_object(:textLeafName, json_build_array(:textValueAsInt))"); + queryParameters.put("textValueAsInt", textValueAsInt); + } + sqlStringBuilder.append(")"); + } + } + + private static void setQueryParameters(final Query query, final Map queryParameters) { + for (final Map.Entry queryParameter : queryParameters.entrySet()) { + query.setParameter(queryParameter.getKey(), queryParameter.getValue()); + } + } + +} -- cgit 1.2.3-korg