diff options
author | ToineSiebelink <toine.siebelink@est.tech> | 2021-07-06 13:03:03 +0100 |
---|---|---|
committer | ToineSiebelink <toine.siebelink@est.tech> | 2021-07-20 16:19:37 +0100 |
commit | f0527c58c17963d940535d0ce0eb934c2b4c635c (patch) | |
tree | f2d8df5b92f3b3e50655fb9a0d685672bf010f0a /cps-ri/src/main | |
parent | 6355b212de77e658b16614eb775f03c7713c8460 (diff) |
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 <toine.siebelink@est.tech>
Change-Id: If422d25cafd2850d25c9a28dea16ba7a5f93dddb
Diffstat (limited to 'cps-ri/src/main')
4 files changed, 161 insertions, 46 deletions
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 844ad84744..6e12d06011 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<FragmentEntity> 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<FragmentEntity> fragmentEntities = + fragmentRepository.findByAnchorAndCpsPath(anchorEntity.getId(), cpsPathQuery); if (cpsPathQuery.hasAncestorAxis()) { final Set<String> 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 4d7e7ff547..c48c79ef65 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<FragmentEntity, Long> {
+public interface FragmentRepository extends JpaRepository<FragmentEntity, Long>, FragmentRepositoryCpsPathQuery {
Optional<FragmentEntity> 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<FragmentEntity, Long> @Query(
value = "SELECT * FROM FRAGMENT WHERE anchor_id = :anchor AND dataspace_id = :dataspace AND parent_id is NULL",
nativeQuery = true)
- List<FragmentEntity> findRootsByDataspaceAndAnchor(
- @Param("dataspace") int dataspaceId, @Param("anchor") int anchorId);
+ List<FragmentEntity> 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<FragmentEntity> findAllByAnchorAndXpathIn(@NonNull AnchorEntity anchorEntity,
- @NonNull Collection<String> xpath);
+ @NonNull Collection<String> xpath);
@Modifying
@Query("DELETE FROM FragmentEntity fe WHERE fe.anchor IN (:anchors)")
void deleteByAnchorIn(@NotNull @Param("anchors") Collection<AnchorEntity> 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<FragmentEntity> 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<FragmentEntity> 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<FragmentEntity> 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 0000000000..04138ecc64 --- /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<FragmentEntity> 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 0000000000..4aa3e5fb36 --- /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<FragmentEntity> findByAnchorAndCpsPath(final int anchorId, final CpsPathQuery cpsPathQuery) { + final var sqlStringBuilder = new StringBuilder("SELECT * FROM FRAGMENT WHERE anchor_id = :anchorId"); + final Map<String, Object> 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<String, Object> 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<String, Object> queryParameters) { + for (final Map.Entry<String, Object> queryParameter : queryParameters.entrySet()) { + query.setParameter(queryParameter.getKey(), queryParameter.getValue()); + } + } + +} |