Merge "Refactor OpenAPI Policy Executor"
diff --git a/docs/api/swagger/policy-executor/openapi.yaml b/docs/api/swagger/policy-executor/openapi.yaml
index 98c5b1e..58ca5ac 100644
--- a/docs/api/swagger/policy-executor/openapi.yaml
+++ b/docs/api/swagger/policy-executor/openapi.yaml
@@ -52,8 +52,12 @@
                 $ref: '#/components/schemas/PolicyExecutionResponse'
         '400':
           $ref: '#/components/responses/BadRequest'
+        '401':
+          $ref: '#/components/responses/Unauthorized'
         '403':
           $ref: '#/components/responses/Forbidden'
+        '409':
+          $ref: '#/components/responses/Conflict'
         '500':
           $ref: '#/components/responses/InternalServerError'
 
@@ -75,48 +79,34 @@
         details:
           type: string
 
-    Payload:
+    Request:
       type: object
       properties:
-        targetFdn:
+        schema:
           type: string
-          description: "The complete FDN (Fully Distinguished Name) for the element to be changed"
-          example: "/Subnetwork=Ireland/MeContext=Athlone/ManagedElement=Athlone/SomeFunction=1/Cell=12"
-        cmHandleId:
-          type: string
-          description: "The CM handle ID (optional)"
-          example: "F811AF64F5146DFC545EC60B73DE948E"
-        resourceIdentifier:
-          type: string
-          description: "The resource identifier (optional)"
-          example: "ManagedElement=Athlone/SomeFunction=1/Cell=12"
-        cmChangeRequest:
+          description: "The schema for the data in this request. The schema name should include the type of operation"
+          example: "org.onap.cps.ncmp.policy-executor:ncmp-create-schema:1.0.0"
+        data:
           type: object
-          description: "The content of the change to be made"
-          example: '{"Cell":[{"id":"Cell-id","attributes":{"administrativeState":"UNLOCKED"}}]}'
+          description: "The data related to the request. The format of the object is determined by the schema"
       required:
-        - targetFdn
-        - cmChangeRequest
+        - schema
+        - data
 
     PolicyExecutionRequest:
       type: object
       properties:
-        payloadType:
-          type: string
-          description: "The type of payload. Currently supported options: 'cm_write'"
-          example: "cm_write"
         decisionType:
           type: string
-          description: "The type of decision. Currently supported options: 'permit'"
-          example: "permit"
-        payload:
+          description: "The type of decision. Currently supported options: 'allow'"
+          example: "allow"
+        requests:
           type: array
           items:
-            $ref: '#/components/schemas/Payload'
+            $ref: '#/components/schemas/Request'
       required:
-        - payloadType
         - decisionType
-        - payload
+        - requests
 
     PolicyExecutionResponse:
       type: object
@@ -127,7 +117,7 @@
           example: "550e8400-e29b-41d4-a716-446655440000"
         decision:
           type: string
-          description: "The decision outcome. Currently supported values: 'permit','deny'"
+          description: "The decision outcome. Currently supported values: 'allow','deny'"
           example: "deny"
         message:
           type: string
@@ -139,16 +129,16 @@
         - message
 
   responses:
-    NotFound:
-      description: "The specified resource was not found"
+    BadRequest:
+      description: "Bad request"
       content:
         application/json:
           schema:
             $ref: '#/components/schemas/ErrorMessage'
           example:
-            status: 404
-            message: "Resource Not Found"
-            details: "The requested resource is not found"
+            status: 400
+            message: "Bad Request"
+            details: "The provided request is not valid"
     Unauthorized:
       description: "Unauthorized request"
       content:
@@ -169,16 +159,16 @@
             status: 403
             message: "Request Forbidden"
             details: "This request is forbidden"
-    BadRequest:
-      description: "Bad request"
+    Conflict:
+      description: "Conflict"
       content:
         application/json:
           schema:
             $ref: '#/components/schemas/ErrorMessage'
           example:
-            status: 400
-            message: "Bad Request"
-            details: "The provided request is not valid"
+            status: 409
+            message: "Conflict"
+            details: "The provided request violates a policy rule"
 
     InternalServerError:
       description: "Internal server error"
diff --git a/docs/schemas/policy-executor/ncmp-create-schema-1.0.0.json b/docs/schemas/policy-executor/ncmp-create-schema-1.0.0.json
new file mode 100644
index 0000000..2ec9daf
--- /dev/null
+++ b/docs/schemas/policy-executor/ncmp-create-schema-1.0.0.json
@@ -0,0 +1,29 @@
+{
+  "$schema": "https://json-schema.org/draft/2019-09/schema",
+  "$id": "urn:cps:org.onap.cps.ncmp.policy-executor:ncmp-create-schema:1.0.0",
+  "$ref": "#/definitions/NcmpCreate",
+  "definitions": {
+    "NcmpCreate": {
+      "type": "object",
+      "additionalProperties": false,
+      "properties": {
+        "cmHandleId": {
+          "type": "string"
+        },
+        "resourceIdentifier": {
+          "type": "string"
+        },
+        "targetIdentifier": {
+          "type": "string"
+        },
+        "cmChangeRequest": {
+          "type": "object"
+        }
+      },
+      "required": [
+        "targetIdentifier",
+        "cmChangeRequest"
+      ]
+    }
+  }
+}
diff --git a/docs/schemas/policy-executor/ncmp-delete-schema-1.0.0.json b/docs/schemas/policy-executor/ncmp-delete-schema-1.0.0.json
new file mode 100644
index 0000000..5df0325
--- /dev/null
+++ b/docs/schemas/policy-executor/ncmp-delete-schema-1.0.0.json
@@ -0,0 +1,25 @@
+{
+  "$schema": "https://json-schema.org/draft/2019-09/schema",
+  "$id": "urn:cps:org.onap.cps.ncmp.policy-executor:ncmp-delete-schema:1.0.0",
+  "$ref": "#/definitions/NcmpDelete",
+  "definitions": {
+    "NcmpDelete": {
+      "type": "object",
+      "additionalProperties": false,
+      "properties": {
+        "cmHandleId": {
+          "type": "string"
+        },
+        "resourceIdentifier": {
+          "type": "string"
+        },
+        "targetIdentifier": {
+          "type": "string"
+        }
+      },
+      "required": [
+        "targetIdentifier"
+      ]
+    }
+  }
+}
diff --git a/docs/schemas/policy-executor/ncmp-patch-schema-1.0.0.json b/docs/schemas/policy-executor/ncmp-patch-schema-1.0.0.json
new file mode 100644
index 0000000..e26c244
--- /dev/null
+++ b/docs/schemas/policy-executor/ncmp-patch-schema-1.0.0.json
@@ -0,0 +1,29 @@
+{
+  "$schema": "https://json-schema.org/draft/2019-09/schema",
+  "$id": "urn:cps:org.onap.cps.ncmp.policy-executor:ncmp-patch-schema:1.0.0",
+  "$ref": "#/definitions/NcmpPatch",
+  "definitions": {
+    "NcmpPatch": {
+      "type": "object",
+      "additionalProperties": false,
+      "properties": {
+        "cmHandleId": {
+          "type": "string"
+        },
+        "resourceIdentifier": {
+          "type": "string"
+        },
+        "targetIdentifier": {
+          "type": "string"
+        },
+        "cmChangeRequest": {
+          "type": "object"
+        }
+      },
+      "required": [
+        "targetIdentifier",
+        "cmChangeRequest"
+      ]
+    }
+  }
+}
diff --git a/docs/schemas/policy-executor/ncmp-update-schema-1.0.0.json b/docs/schemas/policy-executor/ncmp-update-schema-1.0.0.json
new file mode 100644
index 0000000..0a497e3
--- /dev/null
+++ b/docs/schemas/policy-executor/ncmp-update-schema-1.0.0.json
@@ -0,0 +1,29 @@
+{
+  "$schema": "https://json-schema.org/draft/2019-09/schema",
+  "$id": "urn:cps:org.onap.cps.ncmp.policy-executor:ncmp-update-schema:1.0.0",
+  "$ref": "#/definitions/NcmpUpdate",
+  "definitions": {
+    "NcmpUpdate": {
+      "type": "object",
+      "additionalProperties": false,
+      "properties": {
+        "cmHandleId": {
+          "type": "string"
+        },
+        "resourceIdentifier": {
+          "type": "string"
+        },
+        "targetIdentifier": {
+          "type": "string"
+        },
+        "cmChangeRequest": {
+          "type": "object"
+        }
+      },
+      "required": [
+        "targetIdentifier",
+        "cmChangeRequest"
+      ]
+    }
+  }
+}
diff --git a/policy-executor-stub/pom.xml b/policy-executor-stub/pom.xml
index f076a2c..afdc1c7 100644
--- a/policy-executor-stub/pom.xml
+++ b/policy-executor-stub/pom.xml
@@ -22,6 +22,11 @@
     </properties>
 
     <dependencies>
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+            <scope>provided</scope>
+        </dependency>
         <!-- S P R I N G   D E P E N D E N C I E S -->
         <dependency>
             <groupId>org.springframework.boot</groupId>
@@ -163,6 +168,20 @@
                     </execution>
                 </executions>
             </plugin>
+
+            <plugin>
+                <groupId>org.jsonschema2pojo</groupId>
+                <artifactId>jsonschema2pojo-maven-plugin</artifactId>
+                <configuration>
+                    <useJakartaValidation>true</useJakartaValidation>
+                    <sourceDirectory>${project.parent.basedir}/../docs/schemas/policy-executor</sourceDirectory>
+                    <targetPackage>org.onap.cps.policyexecutor.stub.model</targetPackage>
+                    <generateBuilders>true</generateBuilders>
+                    <serializable>true</serializable>
+                    <includeJsr303Annotations>true</includeJsr303Annotations>
+                </configuration>
+            </plugin>
+
         </plugins>
     </build>
 
diff --git a/policy-executor-stub/src/main/java/org/onap/cps/policyexecutor/stub/controller/PolicyExecutorStubController.java b/policy-executor-stub/src/main/java/org/onap/cps/policyexecutor/stub/controller/PolicyExecutorStubController.java
index a5ec6dc..5b3a993 100644
--- a/policy-executor-stub/src/main/java/org/onap/cps/policyexecutor/stub/controller/PolicyExecutorStubController.java
+++ b/policy-executor-stub/src/main/java/org/onap/cps/policyexecutor/stub/controller/PolicyExecutorStubController.java
@@ -20,12 +20,17 @@
 
 package org.onap.cps.policyexecutor.stub.controller;
 
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
 import java.util.Locale;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
+import lombok.RequiredArgsConstructor;
 import org.onap.cps.policyexecutor.stub.api.PolicyExecutorApi;
+import org.onap.cps.policyexecutor.stub.model.NcmpDelete;
 import org.onap.cps.policyexecutor.stub.model.PolicyExecutionRequest;
 import org.onap.cps.policyexecutor.stub.model.PolicyExecutionResponse;
+import org.onap.cps.policyexecutor.stub.model.Request;
 import org.springframework.http.HttpStatus;
 import org.springframework.http.HttpStatusCode;
 import org.springframework.http.ResponseEntity;
@@ -34,9 +39,13 @@
 
 @RestController
 @RequestMapping("${rest.api.policy-executor-base-path}")
+@RequiredArgsConstructor
 public class PolicyExecutorStubController implements PolicyExecutorApi {
 
-    private final Pattern errorCodePattern = Pattern.compile("(\\d{3})");
+    private final ObjectMapper objectMapper;
+
+    private static final Pattern ERROR_CODE_PATTERN = Pattern.compile("(\\d{3})");
+
     private int decisionCounter = 0;
 
     @Override
@@ -44,31 +53,55 @@
                                                      final String action,
                                                      final PolicyExecutionRequest policyExecutionRequest,
                                                      final String authorization) {
-        if (policyExecutionRequest.getPayload().isEmpty()) {
+        if (policyExecutionRequest.getRequests().isEmpty()) {
+            return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
+        }
+        final Request firstRequest = policyExecutionRequest.getRequests().iterator().next();
+        if ("ncmp-delete-schema:1.0.0".equals(firstRequest.getSchema())) {
+            return handleNcmpDeleteSchema(firstRequest);
+        }
+        return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
+    }
+
+    private ResponseEntity<PolicyExecutionResponse> handleNcmpDeleteSchema(final Request request) {
+        final NcmpDelete ncmpDelete;
+        try {
+            ncmpDelete = objectMapper.readValue((String) request.getData(), NcmpDelete.class);
+        } catch (final JsonProcessingException e) {
             return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
         }
 
-        final String firstTargetFdn = policyExecutionRequest.getPayload().iterator().next().getTargetFdn();
+        final String targetIdentifier = ncmpDelete.getTargetIdentifier();
+        if (targetIdentifier == null) {
+            return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
+        }
 
-        final Matcher matcher = errorCodePattern.matcher(firstTargetFdn);
+        final Matcher matcher = ERROR_CODE_PATTERN.matcher(targetIdentifier);
         if (matcher.find()) {
             final int errorCode = Integer.parseInt(matcher.group(1));
             return new ResponseEntity<>(HttpStatusCode.valueOf(errorCode));
         }
 
+        return createPolicyExecutionResponse(targetIdentifier);
+    }
+
+    private ResponseEntity<PolicyExecutionResponse> createPolicyExecutionResponse(final String targetIdentifier) {
         final String decisionId = String.valueOf(++decisionCounter);
         final String decision;
         final String message;
 
-        if (firstTargetFdn.toLowerCase(Locale.getDefault()).contains("cps-is-great")) {
-            decision = "permit";
+        if (targetIdentifier.toLowerCase(Locale.getDefault()).contains("cps-is-great")) {
+            decision = "allow";
             message = "All good";
         } else {
             decision = "deny";
-            message = "Only FDNs containing 'cps-is-great' are permitted";
+            message = "Only FDNs containing 'cps-is-great' are allowed";
         }
+
         final PolicyExecutionResponse policyExecutionResponse =
             new PolicyExecutionResponse(decisionId, decision, message);
+
         return ResponseEntity.ok(policyExecutionResponse);
     }
+
 }
diff --git a/policy-executor-stub/src/test/groovy/org/onap/cps/policyexecutor/stub/controller/PolicyExecutorStubControllerSpec.groovy b/policy-executor-stub/src/test/groovy/org/onap/cps/policyexecutor/stub/controller/PolicyExecutorStubControllerSpec.groovy
index 871db81..efb12ac 100644
--- a/policy-executor-stub/src/test/groovy/org/onap/cps/policyexecutor/stub/controller/PolicyExecutorStubControllerSpec.groovy
+++ b/policy-executor-stub/src/test/groovy/org/onap/cps/policyexecutor/stub/controller/PolicyExecutorStubControllerSpec.groovy
@@ -21,9 +21,10 @@
 package org.onap.cps.policyexecutor.stub.controller
 
 import com.fasterxml.jackson.databind.ObjectMapper
-import org.onap.cps.policyexecutor.stub.model.Payload
+import org.onap.cps.policyexecutor.stub.model.NcmpDelete
 import org.onap.cps.policyexecutor.stub.model.PolicyExecutionRequest
 import org.onap.cps.policyexecutor.stub.model.PolicyExecutionResponse
+import org.onap.cps.policyexecutor.stub.model.Request
 import org.springframework.beans.factory.annotation.Autowired
 import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
 import org.springframework.http.HttpStatus
@@ -44,11 +45,9 @@
 
     def url = '/policy-executor/api/v1/some-action'
 
-    def 'Execute Policy Actions.'() {
-        given: 'a policy execution request with target fdn: #targetFdn'
-            def payload = new Payload(targetFdn, 'some change request')
-            def policyExecutionRequest = new PolicyExecutionRequest('some payload type','some decision type', [payload])
-            def requestBody = objectMapper.writeValueAsString(policyExecutionRequest)
+    def 'Execute policy action.'() {
+        given: 'a policy execution request with target: #targetIdentifier'
+            def requestBody = createRequestBody(targetIdentifier)
         when: 'request is posted'
             def response = mockMvc.perform(post(url)
                     .header('Authorization','some string')
@@ -61,19 +60,17 @@
             def responseBody = response.contentAsString
             def policyExecutionResponse = objectMapper.readValue(responseBody, PolicyExecutionResponse.class)
             assert policyExecutionResponse.decisionId == expectedDecsisonId
-            assert policyExecutionResponse.decision == expectedDecsison
+            assert policyExecutionResponse.decision == expectedDecision
             assert policyExecutionResponse.message == expectedMessage
         where: 'the following targets are used'
-            targetFdn               || expectedDecsisonId | expectedDecsison | expectedMessage
-            'some fdn'              || '1'                | 'deny'           | "Only FDNs containing 'cps-is-great' are permitted"
-            'fdn with cps-is-great' || '2'                | 'permit'         | "All good"
+            targetIdentifier        || expectedDecsisonId | expectedDecision | expectedMessage
+            'some fdn'              || '1'                | 'deny'           | "Only FDNs containing 'cps-is-great' are allowed"
+            'fdn with cps-is-great' || '2'                | 'allow'          | 'All good'
     }
 
-    def 'Execute Policy Action with a HTTP Error Code.'() {
+    def 'Execute policy action with a HTTP error code.'() {
         given: 'a policy execution request with a target fdn with a 3-digit error code'
-            def payload = new Payload('fdn with error code 418', 'some change request')
-            def policyExecutionRequest = new PolicyExecutionRequest('some payload type','some decision type', [payload])
-            def requestBody = objectMapper.writeValueAsString(policyExecutionRequest)
+            def requestBody = createRequestBody('target with error code 418')
         when: 'request is posted'
             def response = mockMvc.perform(post(url)
                 .header('Authorization','some string')
@@ -84,11 +81,9 @@
             assert response.status == 418
     }
 
-    def 'Execute Policy Action without Authorization Header.'() {
+    def 'Execute policy action without authorization header.'() {
         given: 'a valid policy execution request'
-            def payload = new Payload('some fdn', 'some change request')
-            def policyExecutionRequest = new PolicyExecutionRequest('some payload type','some decision type', [payload])
-            def requestBody = objectMapper.writeValueAsString(policyExecutionRequest)
+            def requestBody = createRequestBody('some target')
         when: 'request is posted without authorization header'
             def response = mockMvc.perform(post(url)
                 .contentType(MediaType.APPLICATION_JSON)
@@ -98,9 +93,9 @@
             assert response.status == HttpStatus.OK.value()
     }
 
-    def 'Execute Policy Action with Empty Payload.'() {
-        given: 'a policy execution request with empty payload list'
-            def policyExecutionRequest = new PolicyExecutionRequest('some payload type','some decision type', [])
+    def 'Execute policy action with no requests.'() {
+        given: 'a policy execution request'
+            def policyExecutionRequest = new PolicyExecutionRequest('some decision type', [])
             def requestBody = objectMapper.writeValueAsString(policyExecutionRequest)
         when: 'request is posted'
             def response = mockMvc.perform(post(url)
@@ -112,26 +107,49 @@
             assert response.status == HttpStatus.BAD_REQUEST.value()
     }
 
-    def 'Execute Policy Action without other required attributes.'() {
-        given: 'a policy execution request with payloadType=#payloadType, decisionType=decisionType, targetFdn=#targetFdn, changeRequest=#changeRequest'
-            def payload = new Payload(targetFdn, changeRequest)
-            def policyExecutionRequest = new PolicyExecutionRequest(payloadType, decisionType, [payload])
+    def 'Execute policy action with invalid json for request data.'() {
+        given: 'a policy execution request'
+            def request = new Request('ncmp-delete-schema:1.0.0', 'invalid json')
+            def policyExecutionRequest = new PolicyExecutionRequest('some decision type', [request])
             def requestBody = objectMapper.writeValueAsString(policyExecutionRequest)
         when: 'request is posted'
             def response = mockMvc.perform(post(url)
+                .header('Authorization','some string')
+                .contentType(MediaType.APPLICATION_JSON)
+                .content(requestBody))
+                .andReturn().response
+        then: 'response status is Bad Request'
+            assert response.status == HttpStatus.BAD_REQUEST.value()
+    }
+
+    def 'Execute policy action with missing or invalid attributes.'() {
+        given: 'a policy execution request with decisionType=#decisionType, schema=#schema, targetIdentifier=#targetIdentifier'
+            def requestBody = createRequestBody(decisionType, schema, targetIdentifier)
+        when: 'request is posted'
+            def response = mockMvc.perform(post(url)
                 .header('Authorization','something')
                 .contentType(MediaType.APPLICATION_JSON)
                 .content(requestBody))
                 .andReturn().response
         then: 'response status as expected'
             assert response.status == expectedStatus.value()
-        where: 'following parameters are populated or not'
-            payloadType | decisionType | targetFdn   | changeRequest || expectedStatus
-            'something' | 'something'  | 'something' | 'something'   || HttpStatus.OK
-            null        | 'something'  | 'something' | 'something'   || HttpStatus.BAD_REQUEST
-            'something' | null         | 'something' | 'something'   || HttpStatus.BAD_REQUEST
-            'something' | 'something'  | null        | 'something'   || HttpStatus.BAD_REQUEST
-            'something' | 'something'  | 'something' | null          || HttpStatus.BAD_REQUEST
+        where: 'following parameters are used'
+            decisionType | schema                     | targetIdentifier || expectedStatus
+            'something'  | 'ncmp-delete-schema:1.0.0' | 'something'      || HttpStatus.OK
+            null         | 'ncmp-delete-schema:1.0.0' | 'something'      || HttpStatus.BAD_REQUEST
+            'something'  | 'other schema'             | 'something'      || HttpStatus.BAD_REQUEST
+            'something'  | 'ncmp-delete-schema:1.0.0' | null             || HttpStatus.BAD_REQUEST
+    }
+
+    def createRequestBody(decisionType, schema, targetIdentifier) {
+        def ncmpDelete = new NcmpDelete(targetIdentifier: targetIdentifier)
+        def request = new Request(schema, objectMapper.writeValueAsString(ncmpDelete))
+        def policyExecutionRequest = new PolicyExecutionRequest(decisionType, [request])
+        return objectMapper.writeValueAsString(policyExecutionRequest)
+    }
+
+    def createRequestBody(targetIdentifier) {
+        return createRequestBody('some decision type', 'ncmp-delete-schema:1.0.0', targetIdentifier)
     }
 
 }