CPS-NCMP: Slow cmHandle registration when we use moduleSetTag, alternateId and dataPr... 00/138700/8
authorsourabh_sourabh <sourabh.sourabh@est.tech>
Mon, 12 Aug 2024 09:38:39 +0000 (10:38 +0100)
committersourabh_sourabh <sourabh.sourabh@est.tech>
Tue, 13 Aug 2024 17:06:26 +0000 (18:06 +0100)
- Created a new repo. service for fragment table that executes a native
  sql query to find first ready cm handle id based on moduleset tag and
  then returns list of module references.
- Exposed a new interface into module service that is used by
  module sync service to get list of midule refs by module set tag.

Issue-ID: CPS-2353
Change-Id: I438dbd1ed37c1ff4e15f792e93a095aa604120bc
Signed-off-by: sourabh_sourabh <sourabh.sourabh@est.tech>
cps-application/src/main/resources/application.yml
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/inventory/sync/ModuleSyncService.java
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/inventory/sync/ModuleSyncServiceSpec.groovy
cps-ri/src/main/java/org/onap/cps/spi/impl/CpsModulePersistenceServiceImpl.java
cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentRepositoryCpsPathQueryImpl.java
cps-ri/src/main/java/org/onap/cps/spi/repository/ModuleReferenceQuery.java
cps-ri/src/main/java/org/onap/cps/spi/repository/ModuleReferenceRepositoryImpl.java
cps-service/src/main/java/org/onap/cps/api/CpsModuleService.java
cps-service/src/main/java/org/onap/cps/api/impl/CpsModuleServiceImpl.java
cps-service/src/main/java/org/onap/cps/spi/CpsModulePersistenceService.java
cps-service/src/test/groovy/org/onap/cps/api/impl/CpsModuleServiceImplSpec.groovy

index 83494d6..f61a09b 100644 (file)
@@ -215,7 +215,7 @@ ncmp:
         advised-modules-sync:
             sleep-time-ms: 5000
         locked-modules-sync:
-            sleep-time-ms: 300000
+            sleep-time-ms: 60000
         cm-handle-data-sync:
             sleep-time-ms: 30000
         subscription-forwarding:
index d2bc3ad..ca0f1c6 100644 (file)
@@ -29,23 +29,17 @@ import static org.onap.cps.ncmp.impl.inventory.NcmpPersistence.NFP_OPERATIONAL_D
 import java.time.OffsetDateTime;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.List;
 import java.util.Map;
 import lombok.AllArgsConstructor;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
-import org.apache.commons.lang3.StringUtils;
 import org.onap.cps.api.CpsAnchorService;
 import org.onap.cps.api.CpsDataService;
 import org.onap.cps.api.CpsModuleService;
-import org.onap.cps.ncmp.impl.inventory.CmHandleQueryService;
 import org.onap.cps.ncmp.impl.inventory.models.CmHandleState;
 import org.onap.cps.ncmp.impl.inventory.models.YangModelCmHandle;
-import org.onap.cps.ncmp.impl.utils.YangDataConverter;
 import org.onap.cps.spi.CascadeDeleteAllowed;
-import org.onap.cps.spi.FetchDescendantsOption;
 import org.onap.cps.spi.exceptions.SchemaSetNotFoundException;
-import org.onap.cps.spi.model.DataNode;
 import org.onap.cps.spi.model.ModuleReference;
 import org.onap.cps.utils.ContentType;
 import org.onap.cps.utils.JsonObjectMapper;
@@ -58,7 +52,6 @@ public class ModuleSyncService {
 
     private final DmiModelOperations dmiModelOperations;
     private final CpsModuleService cpsModuleService;
-    private final CmHandleQueryService cmHandleQueryService;
     private final CpsDataService cpsDataService;
     private final CpsAnchorService cpsAnchorService;
     private final JsonObjectMapper jsonObjectMapper;
@@ -113,34 +106,25 @@ public class ModuleSyncService {
     }
 
     private ModuleDelta getModuleDelta(final YangModelCmHandle yangModelCmHandle, final String targetModuleSetTag) {
-        final Collection<ModuleReference> allModuleReferences;
         final Map<String, String> newYangResources;
-
-        final YangModelCmHandle cmHandleWithSameModuleSetTag = getAnyReadyCmHandleByModuleSetTag(targetModuleSetTag);
-        if (cmHandleWithSameModuleSetTag == null) {
+        Collection<ModuleReference> allModuleReferences = getModuleReferencesByModuleSetTag(targetModuleSetTag);
+        if (allModuleReferences.isEmpty()) {
             allModuleReferences = dmiModelOperations.getModuleReferences(yangModelCmHandle);
             newYangResources = dmiModelOperations.getNewYangResourcesFromDmi(yangModelCmHandle,
                     cpsModuleService.identifyNewModuleReferences(allModuleReferences));
         } else {
             log.info("Found other cm handle having same module set tag: {}", targetModuleSetTag);
-            allModuleReferences = cpsModuleService.getYangResourcesModuleReferences(
-                    NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME, cmHandleWithSameModuleSetTag.getId());
             newYangResources = NO_NEW_MODULES;
         }
         return new ModuleDelta(allModuleReferences, newYangResources);
     }
 
-    private YangModelCmHandle getAnyReadyCmHandleByModuleSetTag(final String moduleSetTag) {
-        if (StringUtils.isBlank(moduleSetTag)) {
-            return null;
+    private Collection<ModuleReference> getModuleReferencesByModuleSetTag(final String moduleSetTag) {
+        if (moduleSetTag == null || moduleSetTag.trim().isEmpty()) {
+            return Collections.emptyList();
         }
-        final String escapedModuleSetTag = moduleSetTag.replace("'", "''");
-        final List<DataNode> dataNodes = cmHandleQueryService.queryNcmpRegistryByCpsPath(
-                NCMP_DMI_REGISTRY_PARENT + "/cm-handles[@module-set-tag='" + escapedModuleSetTag + "']",
-                FetchDescendantsOption.DIRECT_CHILDREN_ONLY);
-        return dataNodes.stream().map(YangDataConverter::toYangModelCmHandle)
-                .filter(cmHandle -> cmHandle.getCompositeState().getCmHandleState() == CmHandleState.READY)
-                .findFirst().orElse(null);
+        return cpsModuleService.getModuleReferencesByAttribute(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR,
+                Map.of("module-set-tag", moduleSetTag), Map.of("cm-handle-state", CmHandleState.READY.name()));
     }
 
     private void setCmHandleModuleSetTag(final YangModelCmHandle yangModelCmHandle, final String newModuleSetTag) {
@@ -149,4 +133,5 @@ public class ModuleSyncService {
         cpsDataService.updateNodeLeaves(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, NCMP_DMI_REGISTRY_PARENT,
                 jsonForUpdate, OffsetDateTime.now(), ContentType.JSON);
     }
+
 }
index c3a01a7..6030e5d 100644 (file)
@@ -30,8 +30,6 @@ import org.onap.cps.ncmp.impl.inventory.models.CmHandleState
 import org.onap.cps.ncmp.impl.inventory.models.YangModelCmHandle
 import org.onap.cps.spi.CascadeDeleteAllowed
 import org.onap.cps.spi.exceptions.SchemaSetNotFoundException
-import org.onap.cps.spi.model.DataNode
-import org.onap.cps.spi.model.DataNodeBuilder
 import org.onap.cps.spi.model.ModuleReference
 import org.onap.cps.utils.JsonObjectMapper
 import spock.lang.Specification
@@ -49,13 +47,9 @@ class ModuleSyncServiceSpec extends Specification {
     def mockJsonObjectMapper = Mock(JsonObjectMapper)
 
     def objectUnderTest = new ModuleSyncService(mockDmiModelOperations, mockCpsModuleService,
-            mockCmHandleQueries, mockCpsDataService, mockCpsAnchorService, mockJsonObjectMapper)
+            mockCpsDataService, mockCpsAnchorService, mockJsonObjectMapper)
 
     def expectedDataspaceName = NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME
-    def static cmHandleWithModuleSetTag = new DataNodeBuilder()
-            .withXpath("/dmi-registry/cm-handles[@id='otherId']")
-            .withLeaves(['id': 'otherId', 'module-set-tag': 'tag-1'])
-            .withAnchor('otherId').build()
 
     def 'Sync model for a NEW cm handle using module set tags: #scenario.'() {
         given: 'a cm handle state to be synced'
@@ -70,8 +64,8 @@ class ModuleSyncServiceSpec extends Specification {
             mockDmiModelOperations.getNewYangResourcesFromDmi(yangModelCmHandle, identifiedNewModuleReferences) >> newModuleNameContentToMap
         and: 'the module service identifies #identifiedNewModuleReferences.size() new modules'
             mockCpsModuleService.identifyNewModuleReferences(moduleReferences) >> identifiedNewModuleReferences
-        and: 'system contains other cm handle with "same tag" (that is READY)'
-            mockCmHandleQueries.queryNcmpRegistryByCpsPath(*_) >> existingCmHandlesWithSameTag
+        and: 'the service returns a list of module references when queried with the specified attributes'
+            mockCpsModuleService.getModuleReferencesByAttribute(*_) >> existingModuleReferences
         when: 'module sync is triggered'
             objectUnderTest.syncAndCreateSchemaSetAndAnchor(yangModelCmHandle)
         then: 'create schema set from module is invoked with correct parameters'
@@ -79,10 +73,10 @@ class ModuleSyncServiceSpec extends Specification {
         and: 'anchor is created with the correct parameters'
             1 * mockCpsAnchorService.createAnchor(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME, 'ch-1', 'ch-1')
         where: 'the following parameters are used'
-            scenario                  | existingModuleResourcesInCps         | identifiedNewModuleReferences         | newModuleNameContentToMap     | moduleSetTag | existingCmHandlesWithSameTag
-            'one new module, new tag' | [['module2': '2'], ['module3': '3']] | [new ModuleReference('module1', '1')] | [module1: 'some yang source'] | ''           | []
-            'no new module, new tag'  | [['module1': '1'], ['module2': '2']] | []                                    | [:]                           | 'new-tag-1'  | []
-            'same tag'                | [['module1': '1'], ['module2': '2']] | []                                    | [:]                           | 'same-tag'   | [cmHandleWithModuleSetTag]
+            scenario                  | identifiedNewModuleReferences         | newModuleNameContentToMap     | moduleSetTag | existingModuleReferences
+            'one new module, new tag' | [new ModuleReference('module1', '1')] | [module1: 'some yang source'] | ''           | []
+            'no new module, new tag'  | []                                    | [:]                           | 'new-tag-1'  | []
+            'same tag'                | []                                    | [:]                           | 'same-tag'   | [new ModuleReference('module1', '1'), new ModuleReference('module2', '2')]
     }
 
     def 'Upgrade model for an existing cm handle with Module Set Tag where the modules are #scenario'() {
@@ -101,8 +95,8 @@ class ModuleSyncServiceSpec extends Specification {
             mockCpsModuleService.identifyNewModuleReferences(_) >> []
         and: 'CPS-Core returns list of existing module resources for TBD'
             mockCpsModuleService.getYangResourcesModuleReferences(*_) >> [ new ModuleReference('module1','1') ]
-        and: 'system contains #existingCmHandlesWithSameTag.size() cm handles with same tag'
-            mockCmHandleQueries.queryNcmpRegistryByCpsPath(*_) >> existingCmHandlesWithSameTag
+        and: 'the service returns a list of module references when queried with the specified attributes'
+            mockCpsModuleService.getModuleReferencesByAttribute(*_) >> existingModuleReferences
         and: 'the other cm handle is a state ready'
             mockCmHandleQueries.cmHandleHasState('otherId', CmHandleState.READY) >> true
         when: 'module sync is triggered'
@@ -114,9 +108,9 @@ class ModuleSyncServiceSpec extends Specification {
         and: 'No anchor is created for the upgraded cm handle'
             0 * mockCpsAnchorService.createAnchor(*_)
         where: 'the following parameters are used'
-            scenario      | existingCmHandlesWithSameTag
+            scenario      | existingModuleReferences
             'new'         | []
-            'in database' | [cmHandleWithModuleSetTag]
+            'in database' | [new ModuleReference('module1', '1')]
     }
 
     def 'upgrade model for a existing cm handle'() {
@@ -130,9 +124,8 @@ class ModuleSyncServiceSpec extends Specification {
         and: 'the module service returns some module references'
             def moduleReferences = [new ModuleReference('module1', '1'), new ModuleReference('module2', '2')]
             mockCpsModuleService.getYangResourcesModuleReferences(*_)>> moduleReferences
-        and: 'a cm handle with the same moduleSetTag can be found in the registry'
-            mockCmHandleQueries.queryNcmpRegistryByCpsPath(*_) >> [new DataNode(xpath: '/dmi-registry/cm-handles[@id=\'cmHandleId-1\']', leaves: ['id': 'cmHandleId-1'],
-                    childDataNodes: [new DataNode(xpath: '/dmi-registry/cm-handles[@id=\'cmHandleId-1\']/state', leaves: ['cm-handle-state': 'READY'])])]
+        and: 'the service returns a list of module references when queried with the specified attributes'
+            mockCpsModuleService.getModuleReferencesByAttribute(*_) >> moduleReferences
         when: 'module upgrade is triggered'
             objectUnderTest.syncAndUpgradeSchemaSet(yangModelCmHandle)
         then: 'the upgrade is delegated to the module service (with the correct parameters)'
index 17f13b8..2c4cc74 100755 (executable)
@@ -241,6 +241,15 @@ public class CpsModulePersistenceServiceImpl implements CpsModulePersistenceServ
         return moduleReferenceRepository.identifyNewModuleReferences(moduleReferencesToCheck);
     }
 
+    @Override
+    public Collection<ModuleReference> getModuleReferencesByAttribute(final String dataspaceName,
+                                                                      final String anchorName,
+                                                                      final Map<String, String> parentAttributes,
+                                                                      final Map<String, String> childAttributes) {
+        return moduleReferenceRepository.findModuleReferences(dataspaceName, anchorName, parentAttributes,
+                childAttributes);
+    }
+
     private Set<YangResourceEntity> synchronizeYangResources(
         final Map<String, String> moduleReferenceNameToContentMap) {
         final Map<String, YangResourceEntity> checksumToEntityMap = moduleReferenceNameToContentMap.entrySet().stream()
index 78e0f08..9c98f7f 100644 (file)
@@ -1,6 +1,6 @@
 /*-
  * ============LICENSE_START=======================================================
- *  Copyright (C) 2021-2023 Nordix Foundation.
+ *  Copyright (C) 2021-2024 Nordix Foundation.
  *  Modifications Copyright (C) 2023 TechMahindra Ltd.
  * ================================================================================
  * Licensed under the Apache License, Version 2.0 (the "License");
@@ -21,8 +21,6 @@
 
 package org.onap.cps.spi.repository;
 
-import jakarta.persistence.EntityManager;
-import jakarta.persistence.PersistenceContext;
 import jakarta.persistence.Query;
 import jakarta.transaction.Transactional;
 import java.util.List;
@@ -38,9 +36,6 @@ import org.onap.cps.spi.entities.FragmentEntity;
 @Slf4j
 public class FragmentRepositoryCpsPathQueryImpl implements FragmentRepositoryCpsPathQuery {
 
-    @PersistenceContext
-    private EntityManager entityManager;
-
     private final FragmentQueryBuilder fragmentQueryBuilder;
 
     @Override
index 00e53aa..4082307 100644 (file)
@@ -1,6 +1,6 @@
 /*-
  * ============LICENSE_START=======================================================
- *  Copyright (C) 2022 Nordix Foundation.
+ *  Copyright (C) 2022-2024 Nordix Foundation.
  * ================================================================================
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -21,6 +21,7 @@
 package org.onap.cps.spi.repository;
 
 import java.util.Collection;
+import java.util.Map;
 import org.onap.cps.spi.model.ModuleReference;
 
 /**
@@ -29,4 +30,8 @@ import org.onap.cps.spi.model.ModuleReference;
 public interface ModuleReferenceQuery {
 
     Collection<ModuleReference> identifyNewModuleReferences(final Collection<ModuleReference> moduleReferencesToCheck);
+
+    Collection<ModuleReference> findModuleReferences(final String dataspaceName, final String anchorName,
+                                                     final Map<String, String> parentAttributes,
+                                                     final Map<String, String> childAttributes);
 }
index 454848b..6cc8234 100644 (file)
@@ -22,12 +22,15 @@ package org.onap.cps.spi.repository;
 
 import jakarta.persistence.EntityManager;
 import jakarta.persistence.PersistenceContext;
+import jakarta.persistence.Query;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
-import lombok.AllArgsConstructor;
+import java.util.Map;
+import java.util.stream.Collectors;
+import lombok.RequiredArgsConstructor;
 import lombok.SneakyThrows;
 import lombok.extern.slf4j.Slf4j;
 import org.onap.cps.spi.model.ModuleReference;
@@ -35,13 +38,13 @@ import org.springframework.transaction.annotation.Transactional;
 
 @Slf4j
 @Transactional
-@AllArgsConstructor
+@RequiredArgsConstructor
 public class ModuleReferenceRepositoryImpl implements ModuleReferenceQuery {
 
     @PersistenceContext
     private EntityManager entityManager;
 
-    private TempTableCreator tempTableCreator;
+    private final TempTableCreator tempTableCreator;
 
     @Override
     @SneakyThrows
@@ -66,6 +69,96 @@ public class ModuleReferenceRepositoryImpl implements ModuleReferenceQuery {
         return identifyNewModuleReferencesForCmHandle(tempTableName);
     }
 
+    /**
+     * Finds module references based on specified dataspace, anchor, and attribute filters.
+     * This method constructs and executes a SQL query to retrieve module references. The query applies filters to
+     * parent and child fragments using the provided attribute maps. The `parentAttributes` are used to filter
+     * parent fragments, while `childAttributes` filter child fragments.
+     *
+     * @param dataspaceName    the name of the dataspace to filter on.
+     * @param anchorName       the name of the anchor to filter on.
+     * @param parentAttributes a map of attributes for filtering parent fragments.
+     * @param childAttributes  a map of attributes for filtering child fragments.
+     * @return a collection of {@link ModuleReference} objects that match the specified filters.
+     */
+    @Transactional
+    @SuppressWarnings("unchecked")
+    @Override
+    public Collection<ModuleReference> findModuleReferences(final String dataspaceName, final String anchorName,
+                                                            final Map<String, String> parentAttributes,
+                                                            final Map<String, String> childAttributes) {
+
+        final String parentFragmentWhereClause = buildWhereClause(childAttributes, "parentFragment");
+        final String childFragmentWhereClause = buildWhereClause(parentAttributes, "childFragment");
+
+        final String moduleReferencesSqlQuery = buildModuleReferencesSqlQuery(parentFragmentWhereClause,
+                childFragmentWhereClause);
+
+        final Query query = entityManager.createNativeQuery(moduleReferencesSqlQuery);
+        setQueryParameters(query, parentAttributes, childAttributes, anchorName, dataspaceName);
+        return processQueryResults(query.getResultList());
+    }
+
+    private String buildWhereClause(final Map<String, String> attributes, final String alias) {
+        return attributes.keySet().stream()
+                .map(attributeName -> String.format("%s.attributes->>'%s' = ?", alias, attributeName))
+                .collect(Collectors.joining(" AND "));
+    }
+
+    private void setQueryParameters(final Query query, final Map<String, String> parentAttributes,
+                                    final Map<String, String> childAttributes, final String anchorName,
+                                    final String dataspaceName) {
+        final String childAttributeValue = childAttributes.entrySet().iterator().next().getValue();
+        query.setParameter(1, childAttributeValue);
+
+        final String parentAttributeValue = parentAttributes.entrySet().iterator().next().getValue();
+        query.setParameter(2, parentAttributeValue);
+
+        query.setParameter(3, anchorName);
+        query.setParameter(4, dataspaceName);
+    }
+
+    private String buildModuleReferencesSqlQuery(final String parentFragmentClause, final String childFragmentClause) {
+        return """
+                WITH Fragment AS (
+                    SELECT childFragment.attributes->>'id' AS schema_set_name
+                    FROM fragment parentFragment
+                    JOIN fragment childFragment ON parentFragment.parent_id = childFragment.id
+                    JOIN anchor anchorInfo ON parentFragment.anchor_id = anchorInfo.id
+                    JOIN dataspace dataspaceInfo ON anchorInfo.dataspace_id = dataspaceInfo.id
+                    WHERE %s
+                    AND %s
+                    AND anchorInfo.name = ?
+                    AND dataspaceInfo.name = ?
+                    LIMIT 1
+                ),
+                SchemaSet AS (
+                    SELECT id
+                    FROM schema_set
+                    WHERE name = (SELECT schema_set_name FROM Fragment)
+                )
+                SELECT yangResource.module_name, yangResource.revision
+                FROM yang_resource yangResource
+                JOIN schema_set_yang_resources schemaSetYangResources
+                ON yangResource.id = schemaSetYangResources.yang_resource_id
+                WHERE schemaSetYangResources.schema_set_id = (SELECT id FROM SchemaSet);
+                """.formatted(parentFragmentClause, childFragmentClause);
+    }
+
+    private Collection<ModuleReference> processQueryResults(final List<Object[]> queryResults) {
+        if (queryResults.isEmpty()) {
+            log.info("No module references found for the provided attributes.");
+            return Collections.emptyList();
+        }
+        return queryResults.stream()
+                .map(queryResult -> {
+                    final String name = (String) queryResult[0];
+                    final String revision = (String) queryResult[1];
+                    return new ModuleReference(name, revision);
+                })
+                .collect(Collectors.toList());
+    }
+
     private Collection<ModuleReference> identifyNewModuleReferencesForCmHandle(final String tempTableName) {
         final String sql = String.format(
                 "SELECT %1$s.module_name, %1$s.revision"
@@ -81,7 +174,6 @@ public class ModuleReferenceRepositoryImpl implements ModuleReferenceQuery {
         for (final Object[] row : resultsAsObjects) {
             resultsAsModuleReferences.add(new ModuleReference((String) row[0], (String) row[1]));
         }
-
         return resultsAsModuleReferences;
     }
 }
index bdd3614..931209c 100644 (file)
@@ -155,4 +155,37 @@ public interface CpsModuleService {
     Collection<ModuleReference> identifyNewModuleReferences(
         Collection<ModuleReference> moduleReferencesToCheck);
 
+    /**
+     * Retrieves module references based on the provided dataspace name, anchor name and attribute filters
+     * for both parent and child fragments.
+
+     * This method constructs and executes a SQL query to find module references from a database, using
+     * the specified `dataspaceName`, `anchorName` and two sets of attribute filters: one for parent fragments
+     * and one for child fragments. The method applies these filters to identify the appropriate fragments
+     * and schema sets, and then retrieves the corresponding module references.
+
+     * The SQL query is dynamically built based on the provided attribute filters:
+     * - The `parentAttributes` map is used to filter the parent fragments. The entries in this map are
+     * converted into a WHERE clause for the parent fragments.
+     * - The `childAttributes` map is used to filter the child fragments. This is applied to the child fragments
+     * after filtering the parent fragments.
+     *
+     * @param dataspaceName    the name of the dataspace to filter on. It is used to locate the relevant dataspace
+     *                         in the database.
+     * @param anchorName       the name of the anchor to filter on. It is used to locate the relevant anchor within
+     *                         the dataspace.
+     * @param parentAttributes a map of attributes to filter parent fragments. Each entry in this map represents
+     *                         an attribute key-value pair used in the WHERE clause for parent fragments.
+     * @param childAttributes  a map of attributes to filter child fragments. Each entry in this map represents
+     *                         an attribute key-value pair used in the WHERE clause for child fragments.
+     * @return a collection of {@link ModuleReference} objects that match the given criteria. Each
+     * {@code ModuleReference} contains information about a module's name and revision.
+     * @implNote The method assumes that both `parentAttributes` and `childAttributes` maps contain at least
+     *     one entry. The first entry from `parentAttributes` is used to filter parent fragments,
+     *     and the first entry from `childAttributes` is used to filter child fragments.
+     */
+    Collection<ModuleReference> getModuleReferencesByAttribute(final String dataspaceName, final String anchorName,
+                                                               final Map<String, String> parentAttributes,
+                                                               final Map<String, String> childAttributes);
+
 }
index e6ad9a8..34610f3 100644 (file)
@@ -171,6 +171,17 @@ public class CpsModuleServiceImpl implements CpsModuleService {
         return cpsModulePersistenceService.identifyNewModuleReferences(moduleReferencesToCheck);
     }
 
+    @Timed(value = "cps.module.service.module.reference.query",
+            description = "Time taken to query list of module references")
+    @Override
+    public Collection<ModuleReference> getModuleReferencesByAttribute(final String dataspaceName,
+                                                                      final String anchorName,
+                                                                      final Map<String, String> parentAttributes,
+                                                                      final Map<String, String> childAttributes) {
+        return cpsModulePersistenceService.getModuleReferencesByAttribute(dataspaceName, anchorName, parentAttributes,
+                childAttributes);
+    }
+
     private boolean isCascadeDeleteProhibited(final CascadeDeleteAllowed cascadeDeleteAllowed) {
         return CascadeDeleteAllowed.CASCADE_DELETE_PROHIBITED == cascadeDeleteAllowed;
     }
index eeaaa47..793f38e 100755 (executable)
@@ -153,4 +153,20 @@ public interface CpsModulePersistenceService {
     Collection<ModuleReference> identifyNewModuleReferences(
         Collection<ModuleReference> moduleReferencesToCheck);
 
+    /**
+     * Retrieves module references based on the specified dataspace, anchor, and attribute filters.
+
+     * Constructs and executes a SQL query to find module references by applying filters for parent and child fragments.
+     * Uses `parentAttributes` for filtering parent fragments and `childAttributes` for filtering child fragments.
+     *
+     * @param dataspaceName    the name of the dataspace to filter on.
+     * @param anchorName       the name of the anchor to filter on.
+     * @param parentAttributes a map of attributes for filtering parent fragments.
+     * @param childAttributes  a map of attributes for filtering child fragments.
+     * @return a collection of {@link ModuleReference} objects matching the criteria.
+     */
+    Collection<ModuleReference> getModuleReferencesByAttribute(final String dataspaceName, final String anchorName,
+                                                               final Map<String, String> parentAttributes,
+                                                               final Map<String, String> childAttributes);
+
 }
index ad8c54b..62eba0c 100644 (file)
@@ -238,6 +238,23 @@ class CpsModuleServiceImplSpec extends Specification {
             1 * mockCpsModulePersistenceService.identifyNewModuleReferences(moduleReferencesToCheck)
     }
 
+    def 'Get module references when queried by attributes'() {
+        given: 'a valid dataspace name and anchor name'
+            def dataspaceName = 'someDataspace'
+            def anchorName = 'someAnchor'
+        and: 'a set of parent attributes and child attributes used for filtering'
+            def parentAttributes = ['some-property-key1': 'some-property-val1']
+            def childAttributes = ['some-property-key2': 'some-property-val2']
+        and: 'a list of expected module references returned by the persistence service'
+            def expectedModuleReferences = [new ModuleReference(moduleName: 'some-name', revision: 'some-revision')]
+            mockCpsModulePersistenceService.getModuleReferencesByAttribute(dataspaceName, anchorName, parentAttributes, childAttributes) >> expectedModuleReferences
+        when: 'the method is invoked to retrieve module references by attributes'
+            def actualModuleReferences = objectUnderTest.getModuleReferencesByAttribute(dataspaceName, anchorName, parentAttributes, childAttributes)
+        then: 'the retrieved module references should match the expected module references'
+            assert actualModuleReferences == expectedModuleReferences
+    }
+
+
     def 'Getting module definitions with module name'() {
         given: 'module persistence service returns module definitions for module name'
             def moduleDefinitionsFromPersistenceService = [ new ModuleDefinition('name', 'revision', 'content' ) ]