/*
* ============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.
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 {
/**
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))
}),
/*
* ============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.
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;
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);
}
}
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) {
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);
}
}
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);
}
}
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);
}
}
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);
}
}
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;
}
if (attributesReferenceIncorrect) {
throw new ProvMnSException(httpMethodName, HttpStatus.BAD_REQUEST,
- "Invalid path for content-type " + contentType);
+ "Invalid path for content-type " + contentType, NO_OP);
}
}
/*
* ============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;
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:
}
}
- 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);
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"'
}
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.'() {
<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>
/*
* ============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.
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.
* @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;
}
}
/*
* ============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.
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;
}
@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;
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);
}
}
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
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
content:
application/json:
schema:
- $ref: '#/components/schemas/NotificationSubscriptionsDataSample'
+ $ref: '#/components/schemas/notificationSubscriptionResponseSample'
description: OK
"400":
content:
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
application/json:
examples:
dataSample:
- $ref: '#/components/examples/NotificationSubscriptionsDataSample'
+ $ref: '#/components/examples/notificationSubscriptionRequestSample'
schema:
type: object
required: true
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
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
type: string
title: Module reference object
type: object
- NotificationSubscriptionsDataSample: {}
+ notificationSubscriptionResponseSample: {}
getDeltaByDataspaceAnchorAndPayload_request:
properties:
targetDataAsJsonFile: