Update latest release of 3GPP ProvMnS interface 46/142846/7
authorseanbeirne <sean.beirne@est.tech>
Mon, 5 Jan 2026 13:47:04 +0000 (13:47 +0000)
committerseanbeirne <sean.beirne@est.tech>
Tue, 6 Jan 2026 16:49:03 +0000 (16:49 +0000)
- badOP -> badOp
- 204 response code on delete
- Changes made to make Patch error responses compatible with required
  badOp
- Unintended OpenApi yaml file changes made, legacy changes from
  previous commits

Change-Id: I287a11642f7b58112b1bf5440546fb1c7ac7fde0
Signed-off-by: seanbeirne <sean.beirne@est.tech>
cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/ProvMnS.java
cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/ProvMnSController.java
cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/ProvMnSRestExceptionHandler.java
cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/controller/ProvMnSControllerSpec.groovy
cps-ncmp-service/pom.xml
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/exceptions/ProvMnSException.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/policyexecutor/OperationDetailsFactory.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/provmns/ParameterMapper.java
docs/api/swagger/cps/openapi.yaml

index 2004e93..4601a98 100644 (file)
@@ -1,6 +1,6 @@
 /*
  *  ============LICENSE_START=======================================================
- *  Copyright (C) 2025 OpenInfra Foundation Europe
+ *  Copyright (C) 2025-2026 OpenInfra Foundation Europe
  *  ================================================================================
  *  Licensed under the Apache License, Version 2.0 (the "License");
  *  you may not use this file except in compliance with the License.
@@ -46,7 +46,7 @@ import org.springframework.web.bind.annotation.PutMapping;
 import org.springframework.web.bind.annotation.RequestBody;
 import org.springframework.web.bind.annotation.RequestParam;
 
-@Tag(name = "ProvMnS", description = "Provisioning Management Service")
+@Tag(name = "ProvMnS", description = "Provisioning Management Service; Version 18.6.0")
 public interface ProvMnS {
 
     /**
@@ -141,9 +141,12 @@ public interface ProvMnS {
         description = "With HTTP DELETE one resource is deleted. "
             + "The resources to be deleted is identified with the target URI.",
         responses = {
-            @ApiResponse(responseCode = "200",
-                description = "Success case (\"200 OK\"). This status code is returned, "
-                    + "when the resource has been successfully deleted. The response body is empty."),
+            @ApiResponse(responseCode = "200", description = "The response code is deprecated. Use 204 instead. "
+                + "Success case (\"200 OK\"). This status code is returned, "
+                + "when the resource has been successfully deleted. The response body is empty."),
+            @ApiResponse(responseCode = "204", description = "Success case (\"204 No Content\"). "
+                + "This status code is returned, when the resource has been successfully deleted. "
+                + "The response body is empty."),
             @ApiResponse(responseCode = "422", description = "Invalid Path Exception", content = {
                 @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorMessage.class))
             }),
index f2c6ee3..54204c8 100644 (file)
@@ -1,6 +1,6 @@
 /*
  *  ============LICENSE_START=======================================================
- *  Copyright (C) 2025 OpenInfra Foundation Europe
+ *  Copyright (C) 2025-2026 OpenInfra Foundation Europe
  *  ================================================================================
  *  Licensed under the Apache License, Version 2.0 (the "License");
  *  you may not use this file except in compliance with the License.
@@ -23,6 +23,7 @@ package org.onap.cps.ncmp.rest.controller;
 import static org.onap.cps.ncmp.api.data.models.OperationType.CREATE;
 import static org.onap.cps.ncmp.api.data.models.OperationType.DELETE;
 import static org.onap.cps.ncmp.impl.models.RequiredDmiService.DATA;
+import static org.onap.cps.ncmp.impl.provmns.ParameterMapper.NO_OP;
 
 import io.netty.handler.timeout.TimeoutException;
 import jakarta.servlet.http.HttpServletRequest;
@@ -99,8 +100,8 @@ public class ProvMnSController implements ProvMnS {
             final UrlTemplateParameters urlTemplateParameters = parametersBuilder.createUrlTemplateParametersForRead(
                 yangModelCmHandle, targetFdn, scope, filter, attributes, fields, dataNodeSelector);
             return dmiRestClient.synchronousGetOperation(DATA, urlTemplateParameters);
-        } catch (final Throwable throwable) {
-            throw toProvMnSException(httpServletRequest.getMethod(), throwable);
+        } catch (final Exception exception) {
+            throw toProvMnSException(httpServletRequest.getMethod(), exception, NO_OP);
         }
     }
 
@@ -110,7 +111,7 @@ public class ProvMnSController implements ProvMnS {
         if (patchItems.size() > maxNumberOfPatchOperations) {
             final String title = patchItems.size() + " operations in request, this exceeds the maximum of "
                 + maxNumberOfPatchOperations;
-            throw new ProvMnSException(httpServletRequest.getMethod(), HttpStatus.PAYLOAD_TOO_LARGE, title);
+            throw new ProvMnSException(httpServletRequest.getMethod(), HttpStatus.PAYLOAD_TOO_LARGE, title, NO_OP);
         }
         final RequestParameters requestParameters = parameterMapper.extractRequestParameters(httpServletRequest);
         for (final PatchItem patchItem : patchItems) {
@@ -124,8 +125,8 @@ public class ProvMnSController implements ProvMnS {
                 parametersBuilder.createUrlTemplateParametersForWrite(yangModelCmHandle, targetFdn);
             return dmiRestClient.synchronousPatchOperation(DATA, patchItems, urlTemplateParameters,
                 httpServletRequest.getContentType());
-        } catch (final Throwable throwable) {
-            throw toProvMnSException(httpServletRequest.getMethod(), throwable);
+        } catch (final Exception exception) {
+            throw toProvMnSException(httpServletRequest.getMethod(), exception, NO_OP);
         }
     }
 
@@ -141,8 +142,8 @@ public class ProvMnSController implements ProvMnS {
             final UrlTemplateParameters urlTemplateParameters =
                 parametersBuilder.createUrlTemplateParametersForWrite(yangModelCmHandle, targetFdn);
             return dmiRestClient.synchronousPutOperation(DATA, resource, urlTemplateParameters);
-        } catch (final Throwable throwable) {
-            throw toProvMnSException(httpServletRequest.getMethod(), throwable);
+        } catch (final Exception exception) {
+            throw toProvMnSException(httpServletRequest.getMethod(), exception, NO_OP);
         }
     }
 
@@ -158,8 +159,8 @@ public class ProvMnSController implements ProvMnS {
             final UrlTemplateParameters urlTemplateParameters =
                 parametersBuilder.createUrlTemplateParametersForWrite(yangModelCmHandle, targetFdn);
             return dmiRestClient.synchronousDeleteOperation(DATA, urlTemplateParameters);
-        } catch (final Throwable throwable) {
-            throw toProvMnSException(httpServletRequest.getMethod(), throwable);
+        } catch (final Exception exception) {
+            throw toProvMnSException(httpServletRequest.getMethod(), exception, NO_OP);
         }
     }
 
@@ -171,17 +172,18 @@ public class ProvMnSController implements ProvMnS {
             final YangModelCmHandle yangModelCmHandle = inventoryPersistence.getYangModelCmHandle(cmHandleId);
             if (!StringUtils.hasText(yangModelCmHandle.getDataProducerIdentifier())) {
                 throw new ProvMnSException(requestParameters.getHttpMethodName(), HttpStatus.UNPROCESSABLE_ENTITY,
-                                           PROVMNS_NOT_SUPPORTED_ERROR_MESSAGE);
+                                           PROVMNS_NOT_SUPPORTED_ERROR_MESSAGE, NO_OP);
             }
             if (yangModelCmHandle.getCompositeState().getCmHandleState() != CmHandleState.READY) {
                 final String title = yangModelCmHandle.getId() + " is not in READY state. Current state: "
                     + yangModelCmHandle.getCompositeState().getCmHandleState().name();
-                throw new ProvMnSException(requestParameters.getHttpMethodName(), HttpStatus.NOT_ACCEPTABLE, title);
+                throw new ProvMnSException(requestParameters.getHttpMethodName(), HttpStatus.NOT_ACCEPTABLE,
+                    title, NO_OP);
             }
             return yangModelCmHandle;
         } catch (final NoAlternateIdMatchFoundException noAlternateIdMatchFoundException) {
             final String title = alternateId + " not found";
-            throw new ProvMnSException(requestParameters.getHttpMethodName(), HttpStatus.NOT_FOUND, title);
+            throw new ProvMnSException(requestParameters.getHttpMethodName(), HttpStatus.NOT_FOUND, title, NO_OP);
         }
     }
 
@@ -201,23 +203,29 @@ public class ProvMnSController implements ProvMnS {
             final OperationDetails operationDetails =
                 operationDetailsFactory.buildOperationDetails(requestParameters, patchItem);
             final OperationType operationType = OperationType.fromOperationName(operationDetails.operation());
-            checkPermission(yangModelCmHandle, operationType, requestParameters.toTargetFdn(), operationDetails);
+            try {
+                checkPermission(yangModelCmHandle, operationType, requestParameters.toTargetFdn(), operationDetails);
+            } catch (final Exception exception) {
+                throw toProvMnSException("PATCH", exception, operationType.name());
+            }
         }
     }
 
-    private ProvMnSException toProvMnSException(final String httpMethodName, final Throwable throwable) {
-        if (throwable instanceof ProvMnSException) {
-            return (ProvMnSException) throwable;
+    private ProvMnSException toProvMnSException(final String httpMethodName, final Exception exception,
+                                                final String badOp) {
+        if (exception instanceof ProvMnSException) {
+            return (ProvMnSException) exception;
         }
         final ProvMnSException provMnSException = new ProvMnSException();
         provMnSException.setHttpMethodName(httpMethodName);
-        provMnSException.setTitle(throwable.getMessage());
+        provMnSException.setTitle(exception.getMessage());
+        provMnSException.setBadOp(badOp);
         final HttpStatus httpStatus;
-        if (throwable instanceof PolicyExecutorException) {
+        if (exception instanceof PolicyExecutorException) {
             httpStatus = HttpStatus.CONFLICT;
-        } else if (throwable instanceof DataValidationException) {
+        } else if (exception instanceof DataValidationException) {
             httpStatus = HttpStatus.BAD_REQUEST;
-        } else if (throwable.getCause() instanceof TimeoutException) {
+        } else if (exception.getCause() instanceof TimeoutException) {
             httpStatus = HttpStatus.GATEWAY_TIMEOUT;
         } else {
             httpStatus = HttpStatus.INTERNAL_SERVER_ERROR;
@@ -242,7 +250,7 @@ public class ProvMnSController implements ProvMnS {
         }
         if (attributesReferenceIncorrect) {
             throw new ProvMnSException(httpMethodName, HttpStatus.BAD_REQUEST,
-                                        "Invalid path for content-type " + contentType);
+                                        "Invalid path for content-type " + contentType, NO_OP);
         }
     }
 
index 842483a..eef31ac 100644 (file)
@@ -1,6 +1,6 @@
 /*
  *  ============LICENSE_START=======================================================
- *  Modifications Copyright (C) 2025 OpenInfra Foundation Europe
+ *  Modifications Copyright (C) 2025-2026 OpenInfra Foundation Europe
  *  ================================================================================
  *  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.rest.controller;
 
+import static org.onap.cps.ncmp.impl.provmns.ParameterMapper.NO_OP;
+
 import java.util.Map;
+import java.util.Objects;
 import lombok.AccessLevel;
 import lombok.NoArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
@@ -57,7 +60,11 @@ public class ProvMnSRestExceptionHandler {
     public static ResponseEntity<Object> handleProvMnsExceptions(final ProvMnSException provMnSException) {
         switch (provMnSException.getHttpMethodName()) {
             case "PATCH":
-                return provMnSErrorResponsePatch(provMnSException.getHttpStatus(), provMnSException.getTitle());
+                if (Objects.equals(provMnSException.getBadOp(), NO_OP)) {
+                    return provMnSErrorResponseDefault(provMnSException.getHttpStatus(), provMnSException.getTitle());
+                }
+                return provMnSErrorResponsePatch(provMnSException.getHttpStatus(), provMnSException.getTitle(),
+                        provMnSException.getBadOp());
             case "GET":
                 return provMnSErrorResponseGet(provMnSException.getHttpStatus(), provMnSException.getTitle());
             default:
@@ -65,9 +72,11 @@ public class ProvMnSRestExceptionHandler {
         }
     }
 
-    private static ResponseEntity<Object> provMnSErrorResponsePatch(final HttpStatus httpStatus, final String title) {
+    private static ResponseEntity<Object> provMnSErrorResponsePatch(final HttpStatus httpStatus,
+                                                                    final String title,
+                                                                    final String badOp) {
         final String type = PROVMNS_ERROR_TYPE_PER_ERROR_CODE.get(httpStatus);
-        final ErrorResponsePatch errorResponsePatch = new ErrorResponsePatch(type);
+        final ErrorResponsePatch errorResponsePatch = new ErrorResponsePatch(type, badOp);
         errorResponsePatch.setStatus(String.valueOf(httpStatus.value()));
         errorResponsePatch.setTitle(title);
         return new ResponseEntity<>(errorResponsePatch, httpStatus);
index a876191..b1d9f70 100644 (file)
@@ -162,7 +162,7 @@ class ProvMnSControllerSpec extends Specification {
             exceptionDuringProcessing                           || expectedHttpStatus               || expectedContent
             new NoAlternateIdMatchFoundException('myTarget')    || HttpStatus.NOT_FOUND             || '"title":"/myClass=id1 not found"'
             new Exception("my message", new TimeoutException()) || HttpStatus.GATEWAY_TIMEOUT       || '"title":"my message"'
-            new Throwable("my message")                         || HttpStatus.INTERNAL_SERVER_ERROR || '"title":"my message"'
+            new Exception("my message")                         || HttpStatus.INTERNAL_SERVER_ERROR || '"title":"my message"'
     }
 
 
@@ -269,7 +269,7 @@ class ProvMnSControllerSpec extends Specification {
         where: 'following media types are used'
             scenario             | contentType            | acceptType                  || expectedHttpStatus
             'Content Type Wrong' | MediaType.TEXT_XML     | MediaType.APPLICATION_JSON  || HttpStatus.UNSUPPORTED_MEDIA_TYPE
-            'Accept Type Wrong'  | patchMediaType | MediaType.TEXT_XML || HttpStatus.NOT_ACCEPTABLE
+            'Accept Type Wrong'  | patchMediaType         | MediaType.TEXT_XML          || HttpStatus.NOT_ACCEPTABLE
     }
 
     def 'Patch request with too many operations.'() {
index fe5a729..8f6bc83 100644 (file)
                             <goal>generate</goal>
                         </goals>
                         <configuration>
-                            <inputSpec>https://forge.3gpp.org/rep/all/5G_APIs/-/raw/REL-18/TS28532_ProvMnS.yaml</inputSpec>
+                            <inputSpec>https://forge.3gpp.org/rep/sa5/MnS/-/raw/Rel-18/OpenAPI/TS28532_ProvMnS.yaml</inputSpec>
                             <invokerPackage>org.onap.cps.ncmp.impl.provmns.controller</invokerPackage>
                             <modelPackage>org.onap.cps.ncmp.impl.provmns.model</modelPackage>
                             <apiPackage>org.onap.cps.ncmp.impl.provmns.api</apiPackage>
index 12f87f7..28c451c 100644 (file)
@@ -1,6 +1,6 @@
 /*
  *  ============LICENSE_START=======================================================
- *  Copyright (C) 2025 OpenInfra Foundation Europe. All rights reserved.
+ *  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.
@@ -25,14 +25,15 @@ import lombok.NoArgsConstructor;
 import lombok.Setter;
 import org.springframework.http.HttpStatus;
 
-@NoArgsConstructor
 @Getter
 @Setter
+@NoArgsConstructor
 public class ProvMnSException extends RuntimeException {
 
     private String httpMethodName;
     private HttpStatus httpStatus;
     private String title;
+    private String badOp;
 
     /**
      * Constructor.
@@ -40,14 +41,17 @@ public class ProvMnSException extends RuntimeException {
      * @param httpMethodName  original REST method
      * @param httpStatus      http status to be reported for this exception
      * @param title           3GPP error title (detail)
+     * @param badOp           nullable string to describe operation type in patch operations
      */
     public ProvMnSException(final String httpMethodName,
                             final HttpStatus httpStatus,
-                            final String title) {
+                            final String title,
+                            final String badOp) {
         super(httpMethodName + " failed");
         this.httpMethodName = httpMethodName;
         this.httpStatus = httpStatus;
         this.title = title;
+        this.badOp = badOp;
     }
 
 }
index c780f63..614d95d 100644 (file)
@@ -1,6 +1,6 @@
 /*
  *  ============LICENSE_START=======================================================
- *  Copyright (C) 2025 OpenInfra Foundation Europe
+ *  Copyright (C) 2025-2026 OpenInfra Foundation Europe
  *  ================================================================================
  *  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 OperationDetailsFactory {
                 break;
             default:
                 throw new ProvMnSException("PATCH", HttpStatus.UNPROCESSABLE_ENTITY,
-                    "Unsupported Patch Operation Type: " + patchItem.getOp().getValue());
+                    "Unsupported Patch Operation Type: " + patchItem.getOp().getValue(), patchItem.getOp().getValue());
         }
         return operationDetails;
     }
index 8717bd7..3b6f085 100644 (file)
@@ -30,6 +30,7 @@ import org.springframework.stereotype.Service;
 @RequiredArgsConstructor
 public class ParameterMapper {
 
+    public static final String NO_OP = null;
     private static final String PROVMNS_BASE_PATH = "ProvMnS/v\\d+/";
     private static final String INVALID_PATH_DETAILS_TEMPLATE = "%s not a valid path";
     private static final int PATH_VARIABLES_EXPECTED_LENGTH = 2;
@@ -71,7 +72,7 @@ public class ParameterMapper {
 
     private ProvMnSException createProvMnSException(final String httpMethodName, final String uriPath) {
         final String title = String.format(INVALID_PATH_DETAILS_TEMPLATE, uriPath);
-        return new ProvMnSException(httpMethodName, HttpStatus.UNPROCESSABLE_ENTITY, title);
+        return new ProvMnSException(httpMethodName, HttpStatus.UNPROCESSABLE_ENTITY, title, NO_OP);
     }
 
 }
index 0088b26..6b00856 100644 (file)
@@ -2750,10 +2750,10 @@ paths:
       parameters:
       - description: "For more details on xpath, please refer https://docs.onap.org/projects/onap-cps/en/latest/xpath.html"
         examples:
-          subscription by dataspace xpath:
+          dataspace xpath:
+            value: /dataspaces
+          anchor xpath:
             value: "/dataspaces/dataspace[@name='dataspace01']"
-          subscription by anchor xpath:
-            value: "/dataspaces/dataspace[@name='dataspace01']/anchors/anchor[@name='anchor01']"
         in: query
         name: xpath
         required: true
@@ -2813,10 +2813,10 @@ paths:
       parameters:
       - description: "For more details on xpath, please refer https://docs.onap.org/projects/onap-cps/en/latest/xpath.html"
         examples:
-          subscription by dataspace xpath:
+          dataspace xpath:
+            value: /dataspaces
+          anchor xpath:
             value: "/dataspaces/dataspace[@name='dataspace01']"
-          subscription by anchor xpath:
-            value: "/dataspaces/dataspace[@name='dataspace01']/anchors/anchor[@name='anchor01']"
         in: query
         name: xpath
         required: true
@@ -2828,7 +2828,7 @@ paths:
           content:
             application/json:
               schema:
-                $ref: '#/components/schemas/NotificationSubscriptionsDataSample'
+                $ref: '#/components/schemas/notificationSubscriptionResponseSample'
           description: OK
         "400":
           content:
@@ -2879,10 +2879,10 @@ paths:
       parameters:
       - description: "For more details on xpath, please refer https://docs.onap.org/projects/onap-cps/en/latest/xpath.html"
         examples:
-          subscription by dataspace xpath:
+          dataspace xpath:
+            value: /dataspaces
+          anchor xpath:
             value: "/dataspaces/dataspace[@name='dataspace01']"
-          subscription by anchor xpath:
-            value: "/dataspaces/dataspace[@name='dataspace01']/anchors/anchor[@name='anchor01']"
         in: query
         name: xpath
         required: true
@@ -2894,7 +2894,7 @@ paths:
           application/json:
             examples:
               dataSample:
-                $ref: '#/components/examples/NotificationSubscriptionsDataSample'
+                $ref: '#/components/examples/notificationSubscriptionRequestSample'
             schema:
               type: object
         required: true
@@ -3060,12 +3060,16 @@ components:
               name: SciFi
             - code: 2
               name: kids
-    NotificationSubscriptionsDataSample:
+    notificationSubscriptionRequestSample:
       value:
-        cps-notification-subscriptions:dataspaces:
-          dataspace:
-          - name: dataspace01
-          - name: dataspace02
+        dataspace:
+        - name: my-dataspace
+          anchors:
+            anchor:
+            - name: my-anchor
+              xpaths:
+                xpath:
+                - path: /shops/bookstore
   parameters:
     dataspaceNameInQuery:
       description: dataspace-name
@@ -3259,10 +3263,10 @@ components:
     notificationSubscriptionXpathInQuery:
       description: "For more details on xpath, please refer https://docs.onap.org/projects/onap-cps/en/latest/xpath.html"
       examples:
-        subscription by dataspace xpath:
+        dataspace xpath:
+          value: /dataspaces
+        anchor xpath:
           value: "/dataspaces/dataspace[@name='dataspace01']"
-        subscription by anchor xpath:
-          value: "/dataspaces/dataspace[@name='dataspace01']/anchors/anchor[@name='anchor01']"
       in: query
       name: xpath
       required: true
@@ -3434,7 +3438,7 @@ components:
           type: string
       title: Module reference object
       type: object
-    NotificationSubscriptionsDataSample: {}
+    notificationSubscriptionResponseSample: {}
     getDeltaByDataspaceAnchorAndPayload_request:
       properties:
         targetDataAsJsonFile: