From: seanbeirne Date: Mon, 5 Jan 2026 13:47:04 +0000 (+0000) Subject: Update latest release of 3GPP ProvMnS interface X-Git-Url: https://gerrit.onap.org/r/gitweb?a=commitdiff_plain;h=70345981262dbd5b1910c6dd562d482f34effbd0;p=cps.git Update latest release of 3GPP ProvMnS interface - 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 --- diff --git a/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/ProvMnS.java b/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/ProvMnS.java index 2004e93ce4..4601a98407 100644 --- a/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/ProvMnS.java +++ b/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/ProvMnS.java @@ -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)) }), diff --git a/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/ProvMnSController.java b/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/ProvMnSController.java index f2c6ee3751..54204c8521 100644 --- a/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/ProvMnSController.java +++ b/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/ProvMnSController.java @@ -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); } } diff --git a/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/ProvMnSRestExceptionHandler.java b/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/ProvMnSRestExceptionHandler.java index 842483a17f..eef31ac070 100644 --- a/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/ProvMnSRestExceptionHandler.java +++ b/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/ProvMnSRestExceptionHandler.java @@ -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. @@ -19,7 +19,10 @@ 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 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 provMnSErrorResponsePatch(final HttpStatus httpStatus, final String title) { + private static ResponseEntity 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); diff --git a/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/controller/ProvMnSControllerSpec.groovy b/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/controller/ProvMnSControllerSpec.groovy index a876191e62..b1d9f70b30 100644 --- a/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/controller/ProvMnSControllerSpec.groovy +++ b/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/controller/ProvMnSControllerSpec.groovy @@ -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.'() { diff --git a/cps-ncmp-service/pom.xml b/cps-ncmp-service/pom.xml index fe5a7291e1..8f6bc83ba2 100644 --- a/cps-ncmp-service/pom.xml +++ b/cps-ncmp-service/pom.xml @@ -146,7 +146,7 @@ generate - https://forge.3gpp.org/rep/all/5G_APIs/-/raw/REL-18/TS28532_ProvMnS.yaml + https://forge.3gpp.org/rep/sa5/MnS/-/raw/Rel-18/OpenAPI/TS28532_ProvMnS.yaml org.onap.cps.ncmp.impl.provmns.controller org.onap.cps.ncmp.impl.provmns.model org.onap.cps.ncmp.impl.provmns.api diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/exceptions/ProvMnSException.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/exceptions/ProvMnSException.java index 12f87f77ab..28c451ce20 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/exceptions/ProvMnSException.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/exceptions/ProvMnSException.java @@ -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; } } diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/policyexecutor/OperationDetailsFactory.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/policyexecutor/OperationDetailsFactory.java index c780f639b4..614d95dba4 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/policyexecutor/OperationDetailsFactory.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/policyexecutor/OperationDetailsFactory.java @@ -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; } diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/provmns/ParameterMapper.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/provmns/ParameterMapper.java index 8717bd7d03..3b6f085d62 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/provmns/ParameterMapper.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/provmns/ParameterMapper.java @@ -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); } } diff --git a/docs/api/swagger/cps/openapi.yaml b/docs/api/swagger/cps/openapi.yaml index 0088b26557..6b00856299 100644 --- a/docs/api/swagger/cps/openapi.yaml +++ b/docs/api/swagger/cps/openapi.yaml @@ -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: