Register new Datajob (Bulk) DMIs (Step 2) 62/143562/4
authoregernug <gerard.nugent@est.tech>
Tue, 10 Mar 2026 09:10:12 +0000 (09:10 +0000)
committeregernug <gerard.nugent@est.tech>
Wed, 11 Mar 2026 11:48:56 +0000 (11:48 +0000)
- Added all logic for new Bulk DMIs
- Removed validation od DMI Registration
- Changed WriteRequestExaminer to use DATAJOBS_WRITE
- Changed WebClient logic to include new DMIs
- Refactored service resolution logic with fallback strategy
- Extended test cases to reflect new fields

Issue-ID: CPS-3179

Change-Id: I97490270b0accdff81a70ec7ae96631c6754518d
Signed-off-by: egernug <gerard.nugent@est.tech>
19 files changed:
cps-ncmp-rest/docs/openapi/components.yaml
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/inventory/models/DmiPluginRegistration.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/inventory/models/NcmpServiceCmHandle.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/datajobs/DmiSubJobRequestHandler.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/datajobs/WriteRequestExaminer.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/dmi/DmiRestClient.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/dmi/DmiServiceNameResolver.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/inventory/CmHandleRegistrationService.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/inventory/models/YangModelCmHandle.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/models/RequiredDmiService.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/utils/YangDataConverter.java
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/exceptions/DmiRequestExceptionSpec.groovy [new file with mode: 0644]
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/datajobs/DmiSubJobRequestHandlerSpec.groovy
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/datajobs/WriteRequestExaminerSpec.groovy
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/inventory/CmHandleRegistrationServiceSpec.groovy
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/inventory/models/YangModelCmHandleSpec.groovy
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/utils/YangDataConverterSpec.groovy
docs/api/swagger/cps/openapi.yaml
docs/api/swagger/ncmp/openapi-inventory.yaml

index 002dca1..61521a8 100644 (file)
@@ -1,5 +1,5 @@
 #  ============LICENSE_START=======================================================
-#  Copyright (C) 2021-2025 OpenInfra Foundation Europe. All rights reserved.
+#  Copyright (C) 2021-2026 OpenInfra Foundation Europe. All rights reserved.
 #  Modifications Copyright (C) 2021 Pantheon.tech
 #  Modifications Copyright (C) 2022 Bell Canada
 #  ================================================================================
@@ -64,6 +64,14 @@ components:
           type: string
           example: my-dmi-model-plugin
           default: ""
+        dmiDatajobsReadPlugin:
+          type: string
+          example: my-dmi-datajobs-read-plugin
+          default: ""
+        dmiDatajobsWritePlugin:
+          type: string
+          example: my-dmi-datajobs-write-plugin
+          default: ""
         createdCmHandles:
           type: array
           items:
index 44e2ebe..baf341b 100644 (file)
@@ -1,6 +1,6 @@
 /*
  *  ============LICENSE_START=======================================================
- *  Copyright (C) 2021-2024 Nordix Foundation
+ *  Copyright (C) 2021-2026 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.
@@ -22,13 +22,10 @@ package org.onap.cps.ncmp.api.inventory.models;
 
 import com.fasterxml.jackson.annotation.JsonInclude;
 import com.fasterxml.jackson.annotation.JsonInclude.Include;
-import com.google.common.base.Strings;
 import java.util.Collections;
 import java.util.List;
 import lombok.Getter;
 import lombok.Setter;
-import org.onap.cps.ncmp.api.exceptions.DmiRequestException;
-import org.onap.cps.ncmp.api.exceptions.NcmpException;
 
 /**
  * Dmi Registry request object.
@@ -44,6 +41,10 @@ public class DmiPluginRegistration {
 
     private String dmiModelPlugin;
 
+    private String dmiDatajobsReadPlugin;
+
+    private String dmiDatajobsWritePlugin;
+
     private List<NcmpServiceCmHandle> createdCmHandles = Collections.emptyList();
 
     private List<NcmpServiceCmHandle> updatedCmHandles = Collections.emptyList();
@@ -52,38 +53,4 @@ public class DmiPluginRegistration {
 
     private UpgradedCmHandles upgradedCmHandles = new UpgradedCmHandles();
 
-    /**
-     * Validates plugin service names.
-     * @throws NcmpException if validation fails.
-     */
-    public void validateDmiPluginRegistration() throws NcmpException {
-        final String combinedServiceName = dmiPlugin;
-        final String dataServiceName = dmiDataPlugin;
-        final String modelsServiceName = dmiModelPlugin;
-
-        String errorMessage = null;
-
-        if (isNullEmptyOrBlank(combinedServiceName)) {
-            if ((isNullEmptyOrBlank(dataServiceName) && isNullEmptyOrBlank(modelsServiceName))) {
-                errorMessage = "No DMI plugin service names";
-            } else {
-                if (isNullEmptyOrBlank(dataServiceName) || isNullEmptyOrBlank(modelsServiceName)) {
-                    errorMessage = "Cannot register just a Data or Model plugin service name";
-                }
-            }
-        } else {
-            if (!isNullEmptyOrBlank(dataServiceName) || !isNullEmptyOrBlank(modelsServiceName)) {
-                errorMessage = "Cannot register combined plugin service name and other service names";
-            }
-        }
-
-        if (errorMessage != null) {
-            throw new DmiRequestException(errorMessage, "Please supply correct plugin information.");
-        }
-    }
-
-    public static boolean isNullEmptyOrBlank(final String serviceName) {
-        return Strings.isNullOrEmpty(serviceName) || serviceName.isBlank();
-    }
-
 }
index 2f571f1..85fad90 100644 (file)
@@ -1,6 +1,6 @@
 /*
  *  ============LICENSE_START=======================================================
- *  Copyright (C) 2021-2024 Nordix Foundation
+ *  Copyright (C) 2021-2026 Nordix Foundation
  *  ================================================================================
  *  Licensed under the Apache License, Version 2.0 (the "License");
  *  you may not use this file except in compliance with the License.
@@ -50,6 +50,12 @@ public class NcmpServiceCmHandle {
     @JsonSetter(nulls = Nulls.AS_EMPTY)
     private String dmiModelServiceName;
 
+    @JsonSetter(nulls = Nulls.AS_EMPTY)
+    private String dmiDatajobsReadServiceName;
+
+    @JsonSetter(nulls = Nulls.AS_EMPTY)
+    private String dmiDatajobsWriteServiceName;
+
     @JsonSetter(nulls = Nulls.AS_EMPTY)
     private Map<String, String> additionalProperties = Collections.emptyMap();
 
index fd9707e..f9a463c 100644 (file)
@@ -1,6 +1,6 @@
 /*
  *  ============LICENSE_START=======================================================
- *  Copyright (C) 2024-2025 OpenInfra Foundation Europe. All rights reserved.
+ *  Copyright (C) 2024-2026 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.
@@ -74,7 +74,7 @@ public class DmiSubJobRequestHandler {
             final UrlTemplateParameters urlTemplateParameters = getUrlTemplateParameters(dataJobMetadata.destination(),
                                                                                          producerKey);
             final ResponseEntity<Object> responseEntity = dmiRestClient.synchronousPostOperation(
-                    RequiredDmiService.DATA,
+                    RequiredDmiService.DATAJOBS_WRITE,
                     urlTemplateParameters,
                     jsonObjectMapper.asJsonString(subJobWriteRequest),
                     OperationType.CREATE,
index e6c3db8..ac9aa3a 100644 (file)
@@ -103,7 +103,7 @@ public class WriteRequestExaminer {
 
     private ProducerKey createProducerKey(final YangModelCmHandle yangModelCmHandle) {
         final String dmiDataServiceName =
-                DmiServiceNameResolver.resolveDmiServiceName(RequiredDmiService.DATA, yangModelCmHandle);
+                DmiServiceNameResolver.resolveDmiServiceName(RequiredDmiService.DATAJOBS_WRITE, yangModelCmHandle);
         return new ProducerKey(dmiDataServiceName, yangModelCmHandle.getDataProducerIdentifier());
     }
 
index d1172ce..f1c3078 100644 (file)
@@ -25,7 +25,7 @@ import static org.onap.cps.ncmp.api.NcmpResponseStatus.DMI_SERVICE_NOT_RESPONDIN
 import static org.onap.cps.ncmp.api.NcmpResponseStatus.UNABLE_TO_READ_RESOURCE_DATA;
 import static org.onap.cps.ncmp.api.NcmpResponseStatus.UNKNOWN_ERROR;
 import static org.onap.cps.ncmp.api.data.models.OperationType.READ;
-import static org.onap.cps.ncmp.impl.models.RequiredDmiService.DATA;
+import static org.onap.cps.ncmp.impl.models.RequiredDmiService.MODEL;
 import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR;
 import static org.springframework.http.HttpStatus.REQUEST_TIMEOUT;
 
@@ -250,7 +250,7 @@ public class DmiRestClient {
     }
 
     private WebClient getWebClient(final RequiredDmiService requiredDmiService) {
-        return DATA.equals(requiredDmiService) ? dataServicesWebClient : modelServicesWebClient;
+        return requiredDmiService == MODEL ? modelServicesWebClient : dataServicesWebClient;
     }
 
     private void configureHttpHeaders(final HttpHeaders httpHeaders, final String authorization) {
index e9378da..935322b 100644 (file)
@@ -1,6 +1,6 @@
 /*
  *  ============LICENSE_START=======================================================
- *  Copyright (C) 2024 Nordix Foundation
+ *  Copyright (C) 2024-2026 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.
 
 package org.onap.cps.ncmp.impl.dmi;
 
+import static org.onap.cps.ncmp.impl.models.RequiredDmiService.DATA;
+import static org.onap.cps.ncmp.impl.models.RequiredDmiService.DATAJOBS_READ;
+import static org.onap.cps.ncmp.impl.models.RequiredDmiService.DATAJOBS_WRITE;
+import static org.onap.cps.ncmp.impl.models.RequiredDmiService.MODEL;
+
 import lombok.AccessLevel;
 import lombok.NoArgsConstructor;
 import org.apache.commons.lang3.StringUtils;
@@ -28,6 +33,12 @@ import org.onap.cps.ncmp.api.inventory.models.NcmpServiceCmHandle;
 import org.onap.cps.ncmp.impl.inventory.models.YangModelCmHandle;
 import org.onap.cps.ncmp.impl.models.RequiredDmiService;
 
+/**
+ * Resolves DMI service names based on the required service type.
+ * Resolution follows a priority order: specific service name first, then falls back to the combined service name.
+ * For example, if DATA service is required and dmiDataServiceName is set, it will be used;
+ * otherwise, the combined dmiServiceName will be returned.
+ */
 @NoArgsConstructor(access = AccessLevel.PRIVATE)
 public class DmiServiceNameResolver {
 
@@ -43,7 +54,9 @@ public class DmiServiceNameResolver {
         return resolveDmiServiceName(requiredService,
                 yangModelCmHandle.getDmiServiceName(),
                 yangModelCmHandle.getDmiDataServiceName(),
-                yangModelCmHandle.getDmiModelServiceName());
+                yangModelCmHandle.getDmiModelServiceName(),
+                yangModelCmHandle.getDmiDatajobsReadServiceName(),
+                yangModelCmHandle.getDmiDatajobsWriteServiceName());
     }
 
     /**
@@ -58,7 +71,9 @@ public class DmiServiceNameResolver {
         return resolveDmiServiceName(requiredService,
                 ncmpServiceCmHandle.getDmiServiceName(),
                 ncmpServiceCmHandle.getDmiDataServiceName(),
-                ncmpServiceCmHandle.getDmiModelServiceName());
+                ncmpServiceCmHandle.getDmiModelServiceName(),
+                ncmpServiceCmHandle.getDmiDatajobsReadServiceName(),
+                ncmpServiceCmHandle.getDmiDatajobsWriteServiceName());
     }
 
     /**
@@ -73,19 +88,29 @@ public class DmiServiceNameResolver {
         return resolveDmiServiceName(requiredService,
                 dmiPluginRegistration.getDmiPlugin(),
                 dmiPluginRegistration.getDmiDataPlugin(),
-                dmiPluginRegistration.getDmiModelPlugin());
+                dmiPluginRegistration.getDmiModelPlugin(),
+                dmiPluginRegistration.getDmiDatajobsReadPlugin(),
+                dmiPluginRegistration.getDmiDatajobsWritePlugin());
     }
 
     private static String resolveDmiServiceName(final RequiredDmiService requiredService,
                                                 final String dmiServiceName,
                                                 final String dmiDataServiceName,
-                                                final String dmiModelServiceName) {
-        if (StringUtils.isBlank(dmiServiceName)) {
-            if (RequiredDmiService.DATA.equals(requiredService)) {
-                return dmiDataServiceName;
-            }
+                                                final String dmiModelServiceName,
+                                                final String dmiDatajobsReadServiceName,
+                                                final String dmiDatajobsWriteServiceName) {
+        if (DATA.equals(requiredService) && StringUtils.isNotBlank(dmiDataServiceName)) {
+            return dmiDataServiceName;
+        }
+        if (MODEL.equals(requiredService) && StringUtils.isNotBlank(dmiModelServiceName)) {
             return dmiModelServiceName;
         }
+        if (DATAJOBS_READ.equals(requiredService) && StringUtils.isNotBlank(dmiDatajobsReadServiceName)) {
+            return dmiDatajobsReadServiceName;
+        }
+        if (DATAJOBS_WRITE.equals(requiredService) && StringUtils.isNotBlank(dmiDatajobsWriteServiceName)) {
+            return dmiDatajobsWriteServiceName;
+        }
         return dmiServiceName;
     }
 
index 5327f2f..d64f0f3 100644 (file)
@@ -1,6 +1,6 @@
 /*
  *  ============LICENSE_START=======================================================
- *  Copyright (C) 2021-2025 OpenInfra Foundation Europe. All rights reserved.
+ *  Copyright (C) 2021-2026 OpenInfra Foundation Europe. All rights reserved.
  *  Modifications Copyright (C) 2021 Pantheon.tech
  *  Modifications Copyright (C) 2021-2022 Bell Canada
  *  Modifications Copyright (C) 2023 Deutsche Telekom AG
@@ -93,7 +93,6 @@ public class CmHandleRegistrationService {
      */
     public DmiPluginRegistrationResponse updateDmiRegistration(final DmiPluginRegistration dmiPluginRegistration) {
 
-        dmiPluginRegistration.validateDmiPluginRegistration();
         final DmiPluginRegistrationResponse dmiPluginRegistrationResponse = new DmiPluginRegistrationResponse();
 
         trustLevelManager.registerDmiPlugin(dmiPluginRegistration);
index 858f469..3d14a53 100644 (file)
@@ -102,6 +102,8 @@ public class YangModelCmHandle {
         copy.dmiServiceName = original.getDmiServiceName();
         copy.dmiDataServiceName = original.getDmiDataServiceName();
         copy.dmiModelServiceName = original.getDmiModelServiceName();
+        copy.dmiDatajobsReadServiceName = original.getDmiDatajobsReadServiceName();
+        copy.dmiDatajobsWriteServiceName = original.getDmiDatajobsWriteServiceName();
         copy.compositeState =
                 original.getCompositeState() == null ? null : new CompositeState(original.getCompositeState());
         copy.additionalProperties = original.getAdditionalProperties()
@@ -140,6 +142,8 @@ public class YangModelCmHandle {
         yangModelCmHandle.setDmiServiceName(dmiPluginRegistration.getDmiPlugin());
         yangModelCmHandle.setDmiDataServiceName(dmiPluginRegistration.getDmiDataPlugin());
         yangModelCmHandle.setDmiModelServiceName(dmiPluginRegistration.getDmiModelPlugin());
+        yangModelCmHandle.setDmiDatajobsReadServiceName(dmiPluginRegistration.getDmiDatajobsReadPlugin());
+        yangModelCmHandle.setDmiDatajobsWriteServiceName(dmiPluginRegistration.getDmiDatajobsWritePlugin());
         yangModelCmHandle.setModuleSetTag(StringUtils.trimToEmpty(moduleSetTag));
         yangModelCmHandle.setAlternateId(StringUtils.trimToEmpty(alternateId));
         yangModelCmHandle.setDataProducerIdentifier(StringUtils.trimToEmpty(dataProducerIdentifier));
index cb85205..fa59ecb 100644 (file)
@@ -1,6 +1,6 @@
 /*
  *  ============LICENSE_START=======================================================
- *  Copyright (C) 2021-2025 OpenInfra Foundation Europe. All rights reserved.
+ *  Copyright (C) 2021-2026 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.
@@ -21,8 +21,8 @@
 package org.onap.cps.ncmp.impl.models;
 
 /**
- * Enum to determine if the required service is for a data or model operation.
+ * Enum to determine if the required service is for a data, model, or data job operation.
  */
 public enum RequiredDmiService {
-    DATA, MODEL
+    DATA, MODEL, DATAJOBS_READ, DATAJOBS_WRITE
 }
index a79f2b1..d707a65 100644 (file)
@@ -1,6 +1,6 @@
 /*
  *  ============LICENSE_START=======================================================
- *  Copyright (C) 2022-2025 OpenInfra Foundation Europe. All rights reserved.
+ *  Copyright (C) 2022-2026 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.
@@ -56,6 +56,8 @@ public class YangDataConverter {
         ncmpServiceCmHandle.setDmiServiceName(yangModelCmHandle.getDmiServiceName());
         ncmpServiceCmHandle.setDmiDataServiceName(yangModelCmHandle.getDmiDataServiceName());
         ncmpServiceCmHandle.setDmiModelServiceName(yangModelCmHandle.getDmiModelServiceName());
+        ncmpServiceCmHandle.setDmiDatajobsReadServiceName(yangModelCmHandle.getDmiDatajobsReadServiceName());
+        ncmpServiceCmHandle.setDmiDatajobsWriteServiceName(yangModelCmHandle.getDmiDatajobsWriteServiceName());
         ncmpServiceCmHandle.setCompositeState(yangModelCmHandle.getCompositeState());
         ncmpServiceCmHandle.setModuleSetTag(yangModelCmHandle.getModuleSetTag());
         ncmpServiceCmHandle.setAlternateId(yangModelCmHandle.getAlternateId());
@@ -95,6 +97,10 @@ public class YangDataConverter {
         dmiPluginRegistration.setDmiPlugin(safeGetLeafValue(cmHandleDataNode, "dmi-service-name"));
         dmiPluginRegistration.setDmiDataPlugin(safeGetLeafValue(cmHandleDataNode, "dmi-data-service-name"));
         dmiPluginRegistration.setDmiModelPlugin(safeGetLeafValue(cmHandleDataNode, "dmi-model-service-name"));
+        dmiPluginRegistration.setDmiDatajobsReadPlugin(safeGetLeafValue(
+                cmHandleDataNode, "dmi-datajobs-read-service"));
+        dmiPluginRegistration.setDmiDatajobsWritePlugin(safeGetLeafValue(
+                cmHandleDataNode, "dmi-datajobs-write-service"));
         return YangModelCmHandle.toYangModelCmHandle(
                 dmiPluginRegistration,
                 ncmpServiceCmHandle,
diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/exceptions/DmiRequestExceptionSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/exceptions/DmiRequestExceptionSpec.groovy
new file mode 100644 (file)
index 0000000..d493011
--- /dev/null
@@ -0,0 +1,36 @@
+/*
+ * ============LICENSE_START=======================================================
+ * Copyright (C) 2025-2026 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.api.exceptions
+
+import spock.lang.Specification
+
+class DmiRequestExceptionSpec extends Specification {
+
+    def objectUnderTest = new DmiRequestException('my message', 'my details')
+
+    def 'A DMI request exception.'() {
+        expect: 'the exception has the correct message'
+            objectUnderTest.message == 'my message'
+        and: 'the exception has the correct details'
+            objectUnderTest.details == 'my details'
+    }
+
+}
index b10c96e..e125fa3 100644 (file)
@@ -1,3 +1,24 @@
+/*
+ *  ============LICENSE_START=======================================================
+ *  Copyright (C) 2024-2026 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.datajobs
 
 import com.fasterxml.jackson.databind.ObjectMapper
@@ -8,6 +29,7 @@ import org.onap.cps.ncmp.api.datajobs.models.ProducerKey
 import org.onap.cps.ncmp.impl.dmi.DmiServiceAuthenticationProperties
 import org.onap.cps.ncmp.impl.dmi.DmiRestClient
 import org.onap.cps.ncmp.impl.models.RequiredDmiService
+import org.onap.cps.ncmp.impl.utils.http.UrlTemplateParameters
 import org.onap.cps.utils.JsonObjectMapper
 import org.springframework.http.HttpStatus
 import org.springframework.http.ResponseEntity
@@ -31,7 +53,7 @@ class DmiSubJobRequestHandlerSpec extends Specification {
             def responseAsKeyValuePairs = [subJobId:'my-sub-job-id']
             def responseEntity = new ResponseEntity<>(responseAsKeyValuePairs, HttpStatus.OK)
             def expectedJson = '{"destination":"d1","dataAcceptType":"t1","dataContentType":"t2","dataProducerId":"prod1","dataJobId":"some-job-id","data":[{"path":"p","op":"operation","moduleSetTag":"tag","value":null,"operationId":"o1"}]}'
-            mockDmiRestClient.synchronousPostOperation(RequiredDmiService.DATA, _, expectedJson, OperationType.CREATE, authorization) >> responseEntity
+            mockDmiRestClient.synchronousPostOperation(RequiredDmiService.DATAJOBS_WRITE, _ as UrlTemplateParameters, expectedJson, OperationType.CREATE, authorization) >> responseEntity
         when: 'sending request to DMI invoked'
             objectUnderTest.sendRequestsToDmi(authorization, dataJobId, dataJobMetadata, dmiWriteOperationsPerProducerKey)
         then: 'the result contains the expected sub-job id'
index d950069..e244579 100644 (file)
@@ -1,6 +1,6 @@
 /*
  *  ============LICENSE_START=======================================================
- *  Copyright (C) 2024-2025 OpenInfra Foundation Europe. All rights reserved.
+ *  Copyright (C) 2024-2026 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.
@@ -92,16 +92,16 @@ class WriteRequestExaminerSpec extends Specification {
     }
 
     def 'Validate the creation of a ProducerKey with correct dmiservicename.'() {
-        given: 'yangModelCmHandles with service name: "#dmiServiceName" and data service name: "#dataServiceName"'
-            def yangModelCmHandle = new YangModelCmHandle(dmiServiceName: dmiServiceName, dmiDataServiceName: dataServiceName, dataProducerIdentifier: 'dpi1')
+        given: 'a yang model cm handle'
+            def yangModelCmHandle = new YangModelCmHandle(dmiServiceName: dmiServiceName, dmiDatajobsWriteServiceName: datajobsWriteServiceName, dataProducerIdentifier: 'dpi1')
         when: 'the ProducerKey is created'
             def result = objectUnderTest.createProducerKey(yangModelCmHandle).toString()
-        then: 'we get the ProducerKey with the correct service name'
+        then: 'the ProducerKey has the correct service name'
             assert result == expectedProducerKey
-        where: 'the following services are registered'
-            dmiServiceName     | dataServiceName          || expectedProducerKey
-            'dmi-service-name' | ''                       || 'dmi-service-name#dpi1'
-            ''                 | 'dmi-data-service-name'  || 'dmi-data-service-name#dpi1'
-            'dmi-service-name' | 'dmi-data-service-name'  || 'dmi-service-name#dpi1'
+        where:
+            scenario                                  | dmiServiceName     | datajobsWriteServiceName        || expectedProducerKey
+            'fallback to common service'              | 'dmi-service-name' | ''                              || 'dmi-service-name#dpi1'
+            'specific datajobs write service'         | ''                 | 'dmi-datajobs-write-service'    || 'dmi-datajobs-write-service#dpi1'
+            'datajobs write takes precedence'         | 'dmi-service-name' | 'dmi-datajobs-write-service'    || 'dmi-datajobs-write-service#dpi1'
     }
 }
index 979fba1..b4d02ae 100644 (file)
@@ -1,6 +1,6 @@
 /*
  *  ============LICENSE_START=======================================================
- *  Copyright (C) 2021-2025 OpenInfra Foundation Europe. All rights reserved.
+ *  Copyright (C) 2021-2026 OpenInfra Foundation Europe. All rights reserved.
  *  Modifications Copyright (C) 2022 Bell Canada
  *  ================================================================================
  *  Licensed under the Apache License, Version 2.0 (the "License");
@@ -27,7 +27,6 @@ import org.onap.cps.api.exceptions.AlreadyDefinedException
 import org.onap.cps.api.exceptions.CpsException
 import org.onap.cps.api.exceptions.DataNodeNotFoundException
 import org.onap.cps.api.exceptions.DataValidationException
-import org.onap.cps.ncmp.api.exceptions.DmiRequestException
 import org.onap.cps.ncmp.api.inventory.DataStoreSyncState
 import org.onap.cps.ncmp.api.inventory.models.CmHandleRegistrationResponse
 import org.onap.cps.ncmp.api.inventory.models.CompositeState
@@ -157,46 +156,6 @@ class CmHandleRegistrationServiceSpec extends Specification {
             assert result.upgradedCmHandles[0].ncmpResponseStatus == UNKNOWN_ERROR
     }
 
-    def 'Create CM-handle Validation: Registration with valid Service names: #scenario'() {
-        given: 'a registration '
-            def dmiPluginRegistration = new DmiPluginRegistration(dmiPlugin: dmiPlugin, dmiModelPlugin: dmiModelPlugin,
-                dmiDataPlugin: dmiDataPlugin)
-            dmiPluginRegistration.createdCmHandles = [ncmpServiceCmHandle]
-        when: 'update registration and sync module is called with correct DMI plugin information'
-            objectUnderTest.updateDmiRegistration(dmiPluginRegistration)
-        then: 'create cm handles registration and sync modules is called with the correct plugin information'
-            1 * objectUnderTest.processCreatedCmHandles(dmiPluginRegistration, _)
-        where:
-            scenario                          | dmiPlugin  | dmiModelPlugin | dmiDataPlugin || expectedDmiPluginRegisteredName
-            'combined DMI plugin'             | 'service1' | ''             | ''            || 'service1'
-            'data & model DMI plugins'        | ''         | 'service1'     | 'service2'    || 'service2'
-            'data & model using same service' | ''         | 'service1'     | 'service1'    || 'service1'
-    }
-
-    def 'Create CM-handle Validation: Invalid DMI plugin service name with #scenario'() {
-        given: 'a registration '
-            def dmiPluginRegistration = new DmiPluginRegistration(dmiPlugin: dmiPlugin, dmiModelPlugin: dmiModelPlugin,
-                dmiDataPlugin: dmiDataPlugin)
-            dmiPluginRegistration.createdCmHandles = [ncmpServiceCmHandle]
-        when: 'registration is called with incorrect DMI plugin information'
-            objectUnderTest.updateDmiRegistration(dmiPluginRegistration)
-        then: 'a DMI Request Exception is thrown with correct message details'
-            def exceptionThrown = thrown(DmiRequestException.class)
-            assert exceptionThrown.getMessage().contains(expectedMessageDetails)
-        and: 'registration is not called'
-            0 * objectUnderTest.processCreatedCmHandles(*_)
-        where:
-            scenario                         | dmiPlugin  | dmiModelPlugin | dmiDataPlugin || expectedMessageDetails
-            'empty DMI plugins'              | ''         | ''             | ''            || 'No DMI plugin service names'
-            'blank DMI plugins'              | ' '        | ' '            | ' '           || 'No DMI plugin service names'
-            'null DMI plugins'               | null       | null           | null          || 'No DMI plugin service names'
-            'all DMI plugins'                | 'service1' | 'service2'     | 'service3'    || 'Cannot register combined plugin service name and other service names'
-            '(combined)DMI and Data Plugin'  | 'service1' | ''             | 'service2'    || 'Cannot register combined plugin service name and other service names'
-            '(combined)DMI and model Plugin' | 'service1' | 'service2'     | ''            || 'Cannot register combined plugin service name and other service names'
-            'only model DMI plugin'          | ''         | 'service1'     | ''            || 'Cannot register just a Data or Model plugin service name'
-            'only data DMI plugin'           | ''         | ''             | 'service1'    || 'Cannot register just a Data or Model plugin service name'
-    }
-
     def 'Create CM-Handle Successfully: #scenario.'() {
         given: 'a registration without cm-handle properties'
             def dmiPluginRegistration = new DmiPluginRegistration(dmiPlugin: 'my-server')
@@ -225,6 +184,30 @@ class CmHandleRegistrationServiceSpec extends Specification {
             'without additional & public properties' | [:]                      | [:]                            || [:]                                        | [:]
     }
 
+    def 'Create CM-handle with data jobs plugins: #scenario'() {
+        given: 'a registration with data jobs plugins'
+            def dmiPluginRegistration = new DmiPluginRegistration(
+                dmiPlugin: dmiPlugin,
+                dmiDataPlugin: dmiDataPlugin,
+                dmiModelPlugin: dmiModelPlugin,
+                dmiDatajobsReadPlugin: dmiDatajobsReadPlugin,
+                dmiDatajobsWritePlugin: dmiDatajobsWritePlugin
+            )
+            dmiPluginRegistration.createdCmHandles = [new NcmpServiceCmHandle(cmHandleId: 'cmhandle')]
+        when: 'registration is updated'
+            def response = objectUnderTest.updateDmiRegistration(dmiPluginRegistration)
+        then: 'a successful response is received'
+            response.createdCmHandles.size() == 1
+            response.createdCmHandles[0].status == Status.SUCCESS
+        where:
+            scenario                                    | dmiPlugin  | dmiDataPlugin | dmiModelPlugin | dmiDatajobsReadPlugin   | dmiDatajobsWritePlugin
+            'only data jobs write plugin'               | ''         | ''            | ''             | ''                      | 'datajobs-write-service'
+            'only data jobs read plugin'                | ''         | ''            | ''             | 'datajobs-read-service' | ''
+            'both data jobs plugins'                    | ''         | ''            | ''             | 'datajobs-read-service' | 'datajobs-write-service'
+            'combined with data jobs plugins'           | 'combined' | ''            | ''             | 'datajobs-read-service' | 'datajobs-write-service'
+            'all plugins including data jobs'           | ''         | 'data-svc'    | 'model-svc'    | 'datajobs-read-service' | 'datajobs-write-service'
+    }
+
     def 'Add CM-Handle #scenario.'() {
         given: ' registration details for one cm handles'
             def dmiPluginRegistration = new DmiPluginRegistration(dmiPlugin: 'my-server',
index a9b6b00..acd3810 100644 (file)
@@ -1,6 +1,6 @@
 /*
  *  ============LICENSE_START=======================================================
- *  Copyright (C) 2021-2025 OpenInfra Foundation Europe. All rights reserved.
+ *  Copyright (C) 2021-2026 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.
@@ -30,6 +30,8 @@ import org.onap.cps.ncmp.api.inventory.DataStoreSyncState
 import spock.lang.Specification
 
 import static org.onap.cps.ncmp.impl.models.RequiredDmiService.DATA
+import static org.onap.cps.ncmp.impl.models.RequiredDmiService.DATAJOBS_READ
+import static org.onap.cps.ncmp.impl.models.RequiredDmiService.DATAJOBS_WRITE
 import static org.onap.cps.ncmp.impl.models.RequiredDmiService.MODEL
 
 class YangModelCmHandleSpec extends Specification {
@@ -73,26 +75,27 @@ class YangModelCmHandleSpec extends Specification {
 
     def 'Resolve DMI service name: #scenario and #requiredService service require.'() {
         given: 'a yang model cm handle'
-            def dmiPluginRegistration = new DmiPluginRegistration(
-                    dmiPlugin: dmiServiceName,
-                    dmiDataPlugin: dmiDataServiceName,
-                    dmiModelPlugin: dmiModelServiceName
+            def yangModelCmHandle = new YangModelCmHandle(
+                    dmiServiceName: dmiServiceName,
+                    dmiDataServiceName: dmiDataServiceName,
+                    dmiModelServiceName: dmiModelServiceName,
+                    dmiDatajobsReadServiceName: dmiDatajobsReadServiceName,
+                    dmiDatajobsWriteServiceName: dmiDatajobsWriteServiceName
             )
-            def objectUnderTest = YangModelCmHandle.toYangModelCmHandle(dmiPluginRegistration, new NcmpServiceCmHandle(cmHandleId: 'cm-handle-id-1'),'', '', '', '', '')
         expect:
-            assert objectUnderTest.resolveDmiServiceName(requiredService) == expectedService
+            assert yangModelCmHandle.resolveDmiServiceName(requiredService) == expectedService
         where:
-            scenario                        | dmiServiceName     | dmiDataServiceName | dmiModelServiceName | requiredService || expectedService
-            'common service registered'     | 'common service'   | 'does not matter'  | 'does not matter'   | DATA            || 'common service'
-            'common service registered'     | 'common service'   | 'does not matter'  | 'does not matter'   | MODEL           || 'common service'
-            'common service empty'          | ''                 | 'data service'     | 'does not matter'   | DATA            || 'data service'
-            'common service empty'          | ''                 | 'does not matter'  | 'model service'     | MODEL           || 'model service'
-            'common service blank'          | '   '              | 'data service'     | 'does not matter'   | DATA            || 'data service'
-            'common service blank'          | '   '              | 'does not matter'  | 'model service'     | MODEL           || 'model service'
-            'common service null '          | null               | 'data service'     | 'does not matter'   | DATA            || 'data service'
-            'common service null'           | null               | 'does not matter'  | 'model service'     | MODEL           || 'model service'
-            'only model service registered' | null               | null               | 'does not matter'   | DATA            || null
-            'only data service registered'  | null               | 'does not matter'  | null                | MODEL           || null
+            scenario                                  | dmiServiceName     | dmiDataServiceName | dmiModelServiceName | dmiDatajobsReadServiceName | dmiDatajobsWriteServiceName | requiredService  || expectedService
+            'specific data service registered'        | 'common service'   | 'data service'     | 'does not matter'   | null                       | null                        | DATA             || 'data service'
+            'specific model service registered'       | 'common service'   | 'does not matter'  | 'model service'     | null                       | null                        | MODEL            || 'model service'
+            'specific datajobs read service'          | 'common service'   | 'does not matter'  | 'does not matter'   | 'datajobs-read'            | null                        | DATAJOBS_READ    || 'datajobs-read'
+            'specific datajobs write service'         | 'common service'   | 'does not matter'  | 'does not matter'   | null                       | 'datajobs-write'            | DATAJOBS_WRITE   || 'datajobs-write'
+            'fallback to common for data'             | 'common service'   | null               | 'does not matter'   | null                       | null                        | DATA             || 'common service'
+            'fallback to common for model'            | 'common service'   | 'does not matter'  | null                | null                       | null                        | MODEL            || 'common service'
+            'fallback to common for datajobs read'    | 'common service'   | 'does not matter'  | 'does not matter'   | null                       | null                        | DATAJOBS_READ    || 'common service'
+            'fallback to common for datajobs write'   | 'common service'   | 'does not matter'  | 'does not matter'   | null                       | null                        | DATAJOBS_WRITE   || 'common service'
+            'blank specific ignored, uses fallback'   | 'common service'   | '   '              | 'does not matter'   | null                       | null                        | DATA             || 'common service'
+            'no services available returns null'      | null               | null               | null                | null                       | null                        | DATA             || null
     }
 
     def 'Yang Model Cm Handle Deep Copy.'() {
index 1729d75..53ade25 100644 (file)
@@ -1,6 +1,6 @@
 /*
  * ============LICENSE_START========================================================
- *  Copyright (C) 2022-2025 OpenInfra Foundation Europe. All rights reserved.
+ *  Copyright (C) 2022-2026 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.
@@ -20,6 +20,8 @@
 
 package org.onap.cps.ncmp.impl.utils
 
+import org.onap.cps.ncmp.impl.inventory.models.YangModelCmHandle
+
 import static org.onap.cps.ncmp.api.inventory.models.CmHandleState.ADVISED
 
 import org.onap.cps.api.model.DataNode
@@ -54,6 +56,29 @@ class YangDataConverterSpec extends Specification{
             assert yangModelCmHandle.compositeState.cmHandleState == ADVISED
     }
 
+    def 'Convert a cm handle data node with data jobs service names.'() {
+        given: 'a datanode with data jobs service names'
+            def dataNodeCmHandle = new DataNode(leaves:['id':'ch-1', 'dmi-service-name': 'dmi1', 
+                'dmi-datajobs-read-service': 'dmi-read', 'dmi-datajobs-write-service': 'dmi-write'])
+        when: 'the dataNode is converted'
+            def yangModelCmHandle = YangDataConverter.toYangModelCmHandle(dataNodeCmHandle)
+        then: 'the data jobs service names are set'
+            assert yangModelCmHandle.dmiDatajobsReadServiceName == 'dmi-read'
+            assert yangModelCmHandle.dmiDatajobsWriteServiceName == 'dmi-write'
+    }
+
+    def 'Convert yang model cm handle to ncmp service cm handle with data jobs service names.'() {
+        given: 'a yang model cm handle with data jobs service names'
+            def yangModelCmHandle = new YangModelCmHandle(id: 'ch-1', dmiServiceName: 'dmi1',
+                dmiDatajobsReadServiceName: 'dmi-read', dmiDatajobsWriteServiceName: 'dmi-write',
+                additionalProperties: [], publicProperties: [])
+        when: 'converted to ncmp service cm handle'
+            def ncmpServiceCmHandle = YangDataConverter.toNcmpServiceCmHandle(yangModelCmHandle)
+        then: 'the data jobs service names are preserved'
+            assert ncmpServiceCmHandle.dmiDatajobsReadServiceName == 'dmi-read'
+            assert ncmpServiceCmHandle.dmiDatajobsWriteServiceName == 'dmi-write'
+    }
+
     def 'Convert multiple cm handle data nodes'(){
         given: 'two data nodes in a collection'
             def dataNodes = [new DataNode(xpath:'/dmi-registry/cm-handles[@id=\'some-cm-handle\']', leaves: ['id':'some-cm-handle']),
index 6b00856..fa4cfd7 100644 (file)
@@ -2250,6 +2250,16 @@ paths:
           default: false
           example: true
           type: boolean
+      - description: Content type in header
+        in: header
+        name: Content-Type
+        required: false
+        schema:
+          default: application/json
+          enum:
+          - application/json
+          - application/xml
+          type: string
       responses:
         "200":
           content:
@@ -2259,6 +2269,12 @@ paths:
                   $ref: '#/components/examples/deltaReportSample'
               schema:
                 type: object
+            application/xml:
+              examples:
+                dataSample:
+                  $ref: '#/components/examples/deltaReportSampleXml'
+              schema:
+                type: object
           description: OK
         "400":
           content:
@@ -3040,6 +3056,14 @@ components:
           name: Funny
         target-data:
           name: Comic
+    deltaReportSampleXml:
+      value: "<deltaReports> <deltaReport> <action>replace</action> <xpath>/bookstore/categories[@code='1']</xpath>\
+        \ <source-data> <name>SciFi</name> </source-data> <target-data> <name>Comic</name>\
+        \ </target-data> </deltaReport> <deltaReport> <action>remove</action> <xpath>/bookstore/categories[@code='2']</xpath>\
+        \ <source-data> <code>2</code> <name>kids</name> </source-data> </deltaReport>\
+        \ <deltaReport> <action>create</action> <xpath>/bookstore/categories[@code='3']</xpath>\
+        \ <target-data> <code>3</code> <name>Fiction</name> </target-data> </deltaReport>\
+        \ </deltaReports>"
     dataSampleAcrossAnchors:
       value:
       - anchorName: bookstore1
index 61f6667..98479f6 100644 (file)
@@ -312,6 +312,7 @@ components:
   schemas:
     RestDmiPluginRegistration:
       example:
+        dmiDatajobsReadPlugin: my-dmi-datajobs-read-plugin
         updatedCmHandles:
         - cmHandle: my-cm-handle
           alternateId: "Subnetwork=Europe,ManagedElement=X123"
@@ -359,6 +360,7 @@ components:
           trustLevel: COMPLETE
           dmiProperties: my-dmi-property
         dmiPlugin: my-dmi-plugin
+        dmiDatajobsWritePlugin: my-dmi-datajobs-write-plugin
         dmiModelPlugin: my-dmi-model-plugin
         upgradedCmHandles:
           cmHandles:
@@ -384,6 +386,14 @@ components:
           default: ""
           example: my-dmi-model-plugin
           type: string
+        dmiDatajobsReadPlugin:
+          default: ""
+          example: my-dmi-datajobs-read-plugin
+          type: string
+        dmiDatajobsWritePlugin:
+          default: ""
+          example: my-dmi-datajobs-write-plugin
+          type: string
         createdCmHandles:
           items:
             $ref: '#/components/schemas/RestInputCmHandle'