aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authoraditya puthuparambil <aditya.puthuparambil@bell.ca>2021-08-24 17:44:34 +0100
committerRenu Kumari <renu.kumari@bell.ca>2021-08-25 15:01:06 -0400
commit673c6d94830a1677e685cab82a76747a0808d347 (patch)
tree7eaaee1002bfda4adc20503b4850e8070fdeffc7
parent0b0a89eec95e2cb2671bdd393e94fdaa89b9cc66 (diff)
Add optional observed timestamp in the cps data api
- Added optional query parameter in cps data endpoints - Updated service layer and notification to use observedTimestamp Note: - NCMP REST endpoints are not updated as a part of this patch - NCMP does not sent observed timestamp when using cps data services Issue-ID: CPS-477 Signed-off-by: puthuparambil.aditya <aditya.puthuparambil@bell.ca> Change-Id: I1f92da3da7b3a13c45405fdf44e5fef861991d9a Signed-off-by: Renu Kumari <renu.kumari@bell.ca>
-rwxr-xr-xcps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImpl.java20
-rw-r--r--cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImplSpec.groovy28
-rwxr-xr-xcps-rest/src/main/java/org/onap/cps/rest/controller/DataRestController.java58
-rwxr-xr-xcps-rest/src/main/java/org/onap/cps/rest/exceptions/CpsRestExceptionHandler.java6
-rw-r--r--cps-rest/src/main/resources/static/components.yml8
-rw-r--r--cps-rest/src/main/resources/static/cpsData.yml6
-rwxr-xr-xcps-rest/src/test/groovy/org/onap/cps/rest/controller/DataRestControllerSpec.groovy231
-rw-r--r--cps-rest/src/test/groovy/org/onap/cps/rest/exceptions/CpsRestExceptionHandlerSpec.groovy21
-rw-r--r--cps-rest/src/test/java/org/onap/cps/utils/DateTimeUtility.java40
-rw-r--r--cps-service/src/main/java/org/onap/cps/api/CpsDataService.java66
-rwxr-xr-xcps-service/src/main/java/org/onap/cps/api/impl/CpsDataServiceImpl.java43
-rw-r--r--cps-service/src/main/java/org/onap/cps/notification/CpsDataUpdatedEventFactory.java25
-rw-r--r--cps-service/src/main/java/org/onap/cps/notification/NotificationService.java7
-rw-r--r--cps-service/src/test/groovy/org/onap/cps/api/impl/CpsDataServiceImplSpec.groovy75
-rwxr-xr-xcps-service/src/test/groovy/org/onap/cps/api/impl/E2ENetworkSliceSpec.groovy6
-rw-r--r--cps-service/src/test/groovy/org/onap/cps/notification/CpsDataUpdateEventFactorySpec.groovy37
-rw-r--r--cps-service/src/test/groovy/org/onap/cps/notification/NotificationServiceSpec.groovy17
-rw-r--r--cps-service/src/test/java/org/onap/cps/utils/DateTimeUtility.java40
18 files changed, 508 insertions, 226 deletions
diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImpl.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImpl.java
index 235030a84..b5a591401 100755
--- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImpl.java
+++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImpl.java
@@ -3,6 +3,7 @@
* Copyright (C) 2021 highstreet technologies GmbH
* Modifications Copyright (C) 2021 Nordix Foundation
* Modifications Copyright (C) 2021 Pantheon.tech
+ * Modifications Copyright (C) 2021 Bell Canada
* ================================================================================
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -24,6 +25,7 @@ package org.onap.cps.ncmp.api.impl;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
+import java.time.OffsetDateTime;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
@@ -62,6 +64,8 @@ public class NetworkCmProxyDataServiceImpl implements NetworkCmProxyDataService
private static final String NCMP_DMI_REGISTRY_ANCHOR = "ncmp-dmi-registry";
+ private static final OffsetDateTime NO_TIMESTAMP = null;
+
private CpsDataService cpsDataService;
private ObjectMapper objectMapper;
@@ -105,25 +109,25 @@ public class NetworkCmProxyDataServiceImpl implements NetworkCmProxyDataService
@Override
public void createDataNode(final String cmHandle, final String parentNodeXpath, final String jsonData) {
if (!StringUtils.hasText(parentNodeXpath) || "/".equals(parentNodeXpath)) {
- cpsDataService.saveData(getDataspaceName(), cmHandle, jsonData);
+ cpsDataService.saveData(getDataspaceName(), cmHandle, jsonData, NO_TIMESTAMP);
} else {
- cpsDataService.saveData(getDataspaceName(), cmHandle, parentNodeXpath, jsonData);
+ cpsDataService.saveData(getDataspaceName(), cmHandle, parentNodeXpath, jsonData, NO_TIMESTAMP);
}
}
@Override
public void addListNodeElements(final String cmHandle, final String parentNodeXpath, final String jsonData) {
- cpsDataService.saveListNodeData(getDataspaceName(), cmHandle, parentNodeXpath, jsonData);
+ cpsDataService.saveListNodeData(getDataspaceName(), cmHandle, parentNodeXpath, jsonData, NO_TIMESTAMP);
}
@Override
public void updateNodeLeaves(final String cmHandle, final String parentNodeXpath, final String jsonData) {
- cpsDataService.updateNodeLeaves(getDataspaceName(), cmHandle, parentNodeXpath, jsonData);
+ cpsDataService.updateNodeLeaves(getDataspaceName(), cmHandle, parentNodeXpath, jsonData, NO_TIMESTAMP);
}
@Override
public void replaceNodeTree(final String cmHandle, final String parentNodeXpath, final String jsonData) {
- cpsDataService.replaceNodeTree(getDataspaceName(), cmHandle, parentNodeXpath, jsonData);
+ cpsDataService.replaceNodeTree(getDataspaceName(), cmHandle, parentNodeXpath, jsonData, NO_TIMESTAMP);
}
@Override
@@ -150,7 +154,7 @@ public class NetworkCmProxyDataServiceImpl implements NetworkCmProxyDataService
persistenceCmHandlesList.setCmHandles(createdPersistenceCmHandles);
final String cmHandleJsonData = objectMapper.writeValueAsString(persistenceCmHandlesList);
cpsDataService.saveListNodeData(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, "/dmi-registry",
- cmHandleJsonData);
+ cmHandleJsonData, NO_TIMESTAMP);
} catch (final JsonProcessingException e) {
log.error("Parsing error occurred while converting Object to JSON for Dmi Registry.");
throw new DataValidationException(
@@ -170,7 +174,7 @@ public class NetworkCmProxyDataServiceImpl implements NetworkCmProxyDataService
persistenceCmHandlesList.setCmHandles(updatedPersistenceCmHandles);
final String cmHandlesJsonData = objectMapper.writeValueAsString(persistenceCmHandlesList);
cpsDataService.updateNodeLeavesAndExistingDescendantLeaves(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR,
- "/dmi-registry", cmHandlesJsonData);
+ "/dmi-registry", cmHandlesJsonData, NO_TIMESTAMP);
} catch (final JsonProcessingException e) {
log.error("Parsing error occurred while converting Object to JSON Dmi Registry.");
throw new DataValidationException(
@@ -183,7 +187,7 @@ public class NetworkCmProxyDataServiceImpl implements NetworkCmProxyDataService
for (final String cmHandle: dmiPluginRegistration.getRemovedCmHandles()) {
try {
cpsDataService.deleteListNodeData(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR,
- "/dmi-registry/cm-handles[@id='" + cmHandle + "']");
+ "/dmi-registry/cm-handles[@id='" + cmHandle + "']", NO_TIMESTAMP);
} catch (final DataNodeNotFoundException e) {
log.warn("Datanode {} not deleted message {}", cmHandle, e.getMessage());
}
diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImplSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImplSpec.groovy
index 076016718..45fa0af45 100644
--- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImplSpec.groovy
+++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImplSpec.groovy
@@ -2,6 +2,7 @@
* ============LICENSE_START=======================================================
* Copyright (C) 2021 Nordix Foundation
* Modifications Copyright (C) 2021 Pantheon.tech
+ * Modifications Copyright (C) 2021 Bell Canada
* ================================================================================
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -49,6 +50,7 @@ class NetworkCmProxyDataServiceImplSpec extends Specification {
def objectUnderTest = new NetworkCmProxyDataServiceImpl(mockDmiOperations, mockCpsDataService, mockCpsQueryService, new ObjectMapper())
def cmHandle = 'some handle'
+ def noTimestamp = null
def expectedDataspaceName = 'NFP-Operational'
def 'Query data nodes by cps path with #fetchDescendantsOption.'() {
@@ -67,7 +69,7 @@ class NetworkCmProxyDataServiceImplSpec extends Specification {
when: 'createDataNode is invoked'
objectUnderTest.createDataNode(cmHandle, xpath, jsonData)
then: 'the CPS service method is invoked once with the expected parameters'
- 1 * mockCpsDataService.saveData(expectedDataspaceName, cmHandle, jsonData)
+ 1 * mockCpsDataService.saveData(expectedDataspaceName, cmHandle, jsonData, noTimestamp)
where: 'following parameters were used'
scenario | xpath
'no xpath' | ''
@@ -80,7 +82,7 @@ class NetworkCmProxyDataServiceImplSpec extends Specification {
when: 'createDataNode is invoked'
objectUnderTest.createDataNode(cmHandle, xpath, jsonData)
then: 'the CPS service method is invoked once with the expected parameters'
- 1 * mockCpsDataService.saveData(expectedDataspaceName, cmHandle, xpath, jsonData)
+ 1 * mockCpsDataService.saveData(expectedDataspaceName, cmHandle, xpath, jsonData, noTimestamp)
}
def 'Add list-node elements.'() {
given: 'a cm handle and parent node xpath'
@@ -89,7 +91,7 @@ class NetworkCmProxyDataServiceImplSpec extends Specification {
when: 'addListNodeElements is invoked'
objectUnderTest.addListNodeElements(cmHandle, xpath, jsonData)
then: 'the CPS service method is invoked once with the expected parameters'
- 1 * mockCpsDataService.saveListNodeData(expectedDataspaceName, cmHandle, xpath, jsonData)
+ 1 * mockCpsDataService.saveListNodeData(expectedDataspaceName, cmHandle, xpath, jsonData, noTimestamp)
}
def 'Update data node leaves.'() {
given: 'a cm Handle and a cps path'
@@ -98,7 +100,7 @@ class NetworkCmProxyDataServiceImplSpec extends Specification {
when: 'updateNodeLeaves is invoked'
objectUnderTest.updateNodeLeaves(cmHandle, xpath, jsonData)
then: 'the persistence service is called once with the correct parameters'
- 1 * mockCpsDataService.updateNodeLeaves(expectedDataspaceName, cmHandle, xpath, jsonData)
+ 1 * mockCpsDataService.updateNodeLeaves(expectedDataspaceName, cmHandle, xpath, jsonData, noTimestamp)
}
def 'Replace data node tree.'() {
given: 'a cm Handle and a cps path'
@@ -107,7 +109,7 @@ class NetworkCmProxyDataServiceImplSpec extends Specification {
when: 'replaceNodeTree is invoked'
objectUnderTest.replaceNodeTree(cmHandle, xpath, jsonData)
then: 'the persistence service is called once with the correct parameters'
- 1 * mockCpsDataService.replaceNodeTree(expectedDataspaceName, cmHandle, xpath, jsonData)
+ 1 * mockCpsDataService.replaceNodeTree(expectedDataspaceName, cmHandle, xpath, jsonData, noTimestamp)
}
def 'Register or re-register a DMI Plugin with #scenario cm handles.'() {
@@ -123,11 +125,15 @@ class NetworkCmProxyDataServiceImplSpec extends Specification {
when: 'registration is updated'
objectUnderTest.updateDmiPluginRegistration(dmiPluginRegistration)
then: 'the CPS save list node data is invoked with the expected parameters'
- expectedCallsToSaveNode * mockCpsDataService.saveListNodeData('NCMP-Admin', 'ncmp-dmi-registry', '/dmi-registry', expectedJsonData)
+ expectedCallsToSaveNode * mockCpsDataService.saveListNodeData('NCMP-Admin', 'ncmp-dmi-registry',
+ '/dmi-registry', expectedJsonData, noTimestamp)
and: 'update Node and Child Data Nodes is invoked with correct parameters'
- expectedCallsToUpdateNode * mockCpsDataService.updateNodeLeavesAndExistingDescendantLeaves('NCMP-Admin', 'ncmp-dmi-registry', '/dmi-registry', expectedJsonData)
+ expectedCallsToUpdateNode * mockCpsDataService.updateNodeLeavesAndExistingDescendantLeaves('NCMP-Admin',
+ 'ncmp-dmi-registry', '/dmi-registry', expectedJsonData, noTimestamp)
and : 'delete list data node is invoked with the correct parameters'
- expectedCallsToDeleteListDataNode * mockCpsDataService.deleteListNodeData('NCMP-Admin', 'ncmp-dmi-registry', "/dmi-registry/cm-handles[@id='cmHandle001']")
+ expectedCallsToDeleteListDataNode * mockCpsDataService.deleteListNodeData('NCMP-Admin',
+ 'ncmp-dmi-registry', "/dmi-registry/cm-handles[@id='cmHandle001']", noTimestamp)
+
where:
scenario | createdCmHandles | updatedCmHandles | removedCmHandles || expectedCallsToSaveNode | expectedCallsToUpdateNode | expectedCallsToDeleteListDataNode
'create' | [persistenceCmHandle ] | [] | [] || 1 | 0 | 0
@@ -148,7 +154,8 @@ class NetworkCmProxyDataServiceImplSpec extends Specification {
when: 'registration is updated'
objectUnderTest.updateDmiPluginRegistration(dmiPluginRegistration)
then: 'the CPS save list node data is invoked with the expected parameters'
- 1 * mockCpsDataService.saveListNodeData('NCMP-Admin', 'ncmp-dmi-registry', '/dmi-registry', expectedJsonData)
+ 1 * mockCpsDataService.saveListNodeData('NCMP-Admin', 'ncmp-dmi-registry',
+ '/dmi-registry', expectedJsonData, noTimestamp)
}
def 'Get resource data for pass-through operational from dmi.'() {
@@ -176,7 +183,8 @@ class NetworkCmProxyDataServiceImplSpec extends Specification {
'testFieldQuery',
5,
'testAcceptParam',
- '{"operation":"read","cmHandleProperties":{"testName":"testValue"}}') >> new ResponseEntity<>('result-json', HttpStatus.OK)
+ '{"operation":"read","cmHandleProperties":{"testName":"testValue"}}') >>
+ new ResponseEntity<>('result-json', HttpStatus.OK)
and: 'dmi returns ok response'
response == 'result-json'
}
diff --git a/cps-rest/src/main/java/org/onap/cps/rest/controller/DataRestController.java b/cps-rest/src/main/java/org/onap/cps/rest/controller/DataRestController.java
index 5c79472a4..0e2050e5c 100755
--- a/cps-rest/src/main/java/org/onap/cps/rest/controller/DataRestController.java
+++ b/cps-rest/src/main/java/org/onap/cps/rest/controller/DataRestController.java
@@ -1,6 +1,6 @@
/*
* ============LICENSE_START=======================================================
- * Copyright (C) 2020 Bell Canada.
+ * Copyright (C) 2020-2021 Bell Canada.
* Modifications Copyright (C) 2021 Pantheon.tech
* Modifications Copyright (C) 2021 Nordix Foundation
* ================================================================================
@@ -9,6 +9,7 @@
* 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.
@@ -21,8 +22,10 @@
package org.onap.cps.rest.controller;
-import javax.validation.Valid;
-import javax.validation.constraints.NotNull;
+import java.time.OffsetDateTime;
+import java.time.format.DateTimeFormatter;
+import javax.validation.ValidationException;
+import org.apache.commons.lang3.StringUtils;
import org.onap.cps.api.CpsDataService;
import org.onap.cps.rest.api.CpsDataApi;
import org.onap.cps.spi.FetchDescendantsOption;
@@ -38,25 +41,29 @@ import org.springframework.web.bind.annotation.RestController;
public class DataRestController implements CpsDataApi {
private static final String ROOT_XPATH = "/";
+ private static final String ISO_TIMESTAMP_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSSZ";
+ private static final DateTimeFormatter ISO_TIMESTAMP_FORMATTER = DateTimeFormatter.ofPattern(ISO_TIMESTAMP_FORMAT);
@Autowired
private CpsDataService cpsDataService;
@Override
public ResponseEntity<String> createNode(final String dataspaceName, final String anchorName,
- final String jsonData, final String parentNodeXpath) {
+ final String jsonData, final String parentNodeXpath, final String observedTimestamp) {
if (isRootXpath(parentNodeXpath)) {
- cpsDataService.saveData(dataspaceName, anchorName, jsonData);
+ cpsDataService.saveData(dataspaceName, anchorName, jsonData, toOffsetDateTime(observedTimestamp));
} else {
- cpsDataService.saveData(dataspaceName, anchorName, parentNodeXpath, jsonData);
+ cpsDataService.saveData(dataspaceName, anchorName, parentNodeXpath, jsonData,
+ toOffsetDateTime(observedTimestamp));
}
return new ResponseEntity<>(HttpStatus.CREATED);
}
@Override
public ResponseEntity<String> addListNodeElements(final String parentNodeXpath,
- final String dataspaceName, final String anchorName, final String jsonData) {
- cpsDataService.saveListNodeData(dataspaceName, anchorName, parentNodeXpath, jsonData);
+ final String dataspaceName, final String anchorName, final String jsonData, final String observedTimestamp) {
+ cpsDataService.saveListNodeData(dataspaceName, anchorName, parentNodeXpath, jsonData,
+ toOffsetDateTime(observedTimestamp));
return new ResponseEntity<>(HttpStatus.CREATED);
}
@@ -77,33 +84,48 @@ public class DataRestController implements CpsDataApi {
@Override
public ResponseEntity<Object> updateNodeLeaves(final String dataspaceName,
- final String anchorName, final String jsonData, final String parentNodeXpath) {
- cpsDataService.updateNodeLeaves(dataspaceName, anchorName, parentNodeXpath, jsonData);
+ final String anchorName, final String jsonData, final String parentNodeXpath, final String observedTimestamp) {
+ cpsDataService.updateNodeLeaves(dataspaceName, anchorName, parentNodeXpath, jsonData,
+ toOffsetDateTime(observedTimestamp));
return new ResponseEntity<>(HttpStatus.OK);
}
@Override
- public ResponseEntity<Object> replaceNode(final String dataspaceName,
- final String anchorName, @Valid final String jsonData, @Valid final String parentNodeXpath) {
- cpsDataService.replaceNodeTree(dataspaceName, anchorName, parentNodeXpath, jsonData);
+ public ResponseEntity<Object> replaceNode(final String dataspaceName, final String anchorName,
+ final String jsonData, final String parentNodeXpath, final String observedTimestamp) {
+ cpsDataService
+ .replaceNodeTree(dataspaceName, anchorName, parentNodeXpath, jsonData, toOffsetDateTime(observedTimestamp));
return new ResponseEntity<>(HttpStatus.OK);
}
@Override
- public ResponseEntity<String> replaceListNodeElements(@NotNull @Valid final String parentNodeXpath,
- final String dataspaceName, final String anchorName, @Valid final String jsonData) {
- cpsDataService.replaceListNodeData(dataspaceName, anchorName, parentNodeXpath, jsonData);
+ public ResponseEntity<String> replaceListNodeElements(final String parentNodeXpath,
+ final String dataspaceName, final String anchorName, final String jsonData,
+ final String observedTimestamp) {
+ cpsDataService.replaceListNodeData(dataspaceName, anchorName, parentNodeXpath, jsonData,
+ toOffsetDateTime(observedTimestamp));
return new ResponseEntity<>(HttpStatus.OK);
}
@Override
public ResponseEntity<Void> deleteListNodeElements(final String dataspaceName, final String anchorName,
- final String listNodeXpath) {
- cpsDataService.deleteListNodeData(dataspaceName, anchorName, listNodeXpath);
+ final String listNodeXpath, final String observedTimestamp) {
+ cpsDataService
+ .deleteListNodeData(dataspaceName, anchorName, listNodeXpath, toOffsetDateTime(observedTimestamp));
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}
private static boolean isRootXpath(final String xpath) {
return ROOT_XPATH.equals(xpath);
}
+
+ private OffsetDateTime toOffsetDateTime(final String datetTimestamp) {
+ try {
+ return StringUtils.isEmpty(datetTimestamp)
+ ? null : OffsetDateTime.parse(datetTimestamp, ISO_TIMESTAMP_FORMATTER);
+ } catch (final Exception exception) {
+ throw new ValidationException(
+ String.format("observed-timestamp must be in '%s' format", ISO_TIMESTAMP_FORMAT));
+ }
+ }
}
diff --git a/cps-rest/src/main/java/org/onap/cps/rest/exceptions/CpsRestExceptionHandler.java b/cps-rest/src/main/java/org/onap/cps/rest/exceptions/CpsRestExceptionHandler.java
index 143ad8b88..d790e0802 100755
--- a/cps-rest/src/main/java/org/onap/cps/rest/exceptions/CpsRestExceptionHandler.java
+++ b/cps-rest/src/main/java/org/onap/cps/rest/exceptions/CpsRestExceptionHandler.java
@@ -22,6 +22,7 @@
package org.onap.cps.rest.exceptions;
import javax.servlet.http.HttpServletRequest;
+import javax.validation.ValidationException;
import lombok.extern.slf4j.Slf4j;
import org.onap.cps.rest.controller.AdminRestController;
import org.onap.cps.rest.controller.DataRestController;
@@ -67,6 +68,11 @@ public class CpsRestExceptionHandler {
return buildErrorResponse(HttpStatus.BAD_REQUEST, exception);
}
+ @ExceptionHandler({ValidationException.class})
+ public static ResponseEntity<Object> handleBadRequestExceptions(final ValidationException validationException) {
+ return buildErrorResponse(HttpStatus.BAD_REQUEST, validationException);
+ }
+
@ExceptionHandler({NotFoundInDataspaceException.class, DataNodeNotFoundException.class})
public static ResponseEntity<Object> handleNotFoundExceptions(final CpsException exception,
final HttpServletRequest request) {
diff --git a/cps-rest/src/main/resources/static/components.yml b/cps-rest/src/main/resources/static/components.yml
index 51a49a6e9..75a6f99fc 100644
--- a/cps-rest/src/main/resources/static/components.yml
+++ b/cps-rest/src/main/resources/static/components.yml
@@ -158,6 +158,14 @@ components:
schema:
type: boolean
default: false
+ observedTimestampInQuery:
+ name: observed-timestamp
+ in: query
+ description: observed-timestamp
+ required: false
+ schema:
+ type: string
+ example: '2021-03-21T00:10:34.030-0100'
responses:
NotFound:
diff --git a/cps-rest/src/main/resources/static/cpsData.yml b/cps-rest/src/main/resources/static/cpsData.yml
index 9c4f3334e..75d954473 100644
--- a/cps-rest/src/main/resources/static/cpsData.yml
+++ b/cps-rest/src/main/resources/static/cpsData.yml
@@ -55,6 +55,7 @@ listNodeByDataspaceAndAnchor:
- $ref: 'components.yml#/components/parameters/dataspaceNameInPath'
- $ref: 'components.yml#/components/parameters/anchorNameInPath'
- $ref: 'components.yml#/components/parameters/requiredXpathInQuery'
+ - $ref: 'components.yml#/components/parameters/observedTimestampInQuery'
requestBody:
required: true
content:
@@ -81,6 +82,7 @@ listNodeByDataspaceAndAnchor:
- $ref: 'components.yml#/components/parameters/dataspaceNameInPath'
- $ref: 'components.yml#/components/parameters/anchorNameInPath'
- $ref: 'components.yml#/components/parameters/requiredXpathInQuery'
+ - $ref: 'components.yml#/components/parameters/observedTimestampInQuery'
requestBody:
required: true
content:
@@ -107,6 +109,7 @@ listNodeByDataspaceAndAnchor:
- $ref: 'components.yml#/components/parameters/dataspaceNameInPath'
- $ref: 'components.yml#/components/parameters/anchorNameInPath'
- $ref: 'components.yml#/components/parameters/requiredXpathInQuery'
+ - $ref: 'components.yml#/components/parameters/observedTimestampInQuery'
responses:
'204':
$ref: 'components.yml#/components/responses/NoContent'
@@ -128,6 +131,7 @@ nodesByDataspaceAndAnchor:
- $ref: 'components.yml#/components/parameters/dataspaceNameInPath'
- $ref: 'components.yml#/components/parameters/anchorNameInPath'
- $ref: 'components.yml#/components/parameters/xpathInQuery'
+ - $ref: 'components.yml#/components/parameters/observedTimestampInQuery'
requestBody:
required: true
content:
@@ -154,6 +158,7 @@ nodesByDataspaceAndAnchor:
- $ref: 'components.yml#/components/parameters/dataspaceNameInPath'
- $ref: 'components.yml#/components/parameters/anchorNameInPath'
- $ref: 'components.yml#/components/parameters/xpathInQuery'
+ - $ref: 'components.yml#/components/parameters/observedTimestampInQuery'
requestBody:
required: true
content:
@@ -180,6 +185,7 @@ nodesByDataspaceAndAnchor:
- $ref: 'components.yml#/components/parameters/dataspaceNameInPath'
- $ref: 'components.yml#/components/parameters/anchorNameInPath'
- $ref: 'components.yml#/components/parameters/xpathInQuery'
+ - $ref: 'components.yml#/components/parameters/observedTimestampInQuery'
requestBody:
required: true
content:
diff --git a/cps-rest/src/test/groovy/org/onap/cps/rest/controller/DataRestControllerSpec.groovy b/cps-rest/src/test/groovy/org/onap/cps/rest/controller/DataRestControllerSpec.groovy
index d3d42e306..1d51ec4ac 100755
--- a/cps-rest/src/test/groovy/org/onap/cps/rest/controller/DataRestControllerSpec.groovy
+++ b/cps-rest/src/test/groovy/org/onap/cps/rest/controller/DataRestControllerSpec.groovy
@@ -30,13 +30,10 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilder
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put
-import org.modelmapper.ModelMapper
-import org.onap.cps.api.CpsAdminService
import org.onap.cps.api.CpsDataService
-import org.onap.cps.api.CpsModuleService
-import org.onap.cps.api.CpsQueryService
import org.onap.cps.spi.model.DataNode
import org.onap.cps.spi.model.DataNodeBuilder
+import org.onap.cps.utils.DateTimeUtility
import org.spockframework.spring.SpringBean
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.beans.factory.annotation.Value
@@ -62,14 +59,15 @@ class DataRestControllerSpec extends Specification {
def dataNodeBaseEndpoint
def dataspaceName = 'my_dataspace'
def anchorName = 'my_anchor'
+ def noTimestamp = null
@Shared
static DataNode dataNodeWithLeavesNoChildren = new DataNodeBuilder().withXpath('/xpath')
- .withLeaves([leaf: 'value', leafList: ['leaveListElement1', 'leaveListElement2']]).build()
+ .withLeaves([leaf: 'value', leafList: ['leaveListElement1', 'leaveListElement2']]).build()
@Shared
static DataNode dataNodeWithChild = new DataNodeBuilder().withXpath('/parent')
- .withChildDataNodes([new DataNodeBuilder().withXpath("/parent/child").build()]).build()
+ .withChildDataNodes([new DataNodeBuilder().withXpath("/parent/child").build()]).build()
def setup() {
dataNodeBaseEndpoint = "$basePath/v1/dataspaces/$dataspaceName"
@@ -81,22 +79,46 @@ class DataRestControllerSpec extends Specification {
def json = 'some json (this is not validated)'
when: 'post is invoked with datanode endpoint and json'
def response =
- mvc.perform(
- post(endpoint)
- .contentType(MediaType.APPLICATION_JSON)
- .param('xpath', parentNodeXpath)
- .content(json)
- ).andReturn().response
+ mvc.perform(
+ post(endpoint)
+ .contentType(MediaType.APPLICATION_JSON)
+ .param('xpath', parentNodeXpath)
+ .content(json)
+ ).andReturn().response
then: 'a created response is returned'
response.status == HttpStatus.CREATED.value()
then: 'the java API was called with the correct parameters'
- 1 * mockCpsDataService.saveData(dataspaceName, anchorName, json)
+ 1 * mockCpsDataService.saveData(dataspaceName, anchorName, json, noTimestamp)
where: 'following xpath parameters are are used'
scenario | parentNodeXpath
'no xpath parameter' | ''
'xpath parameter point root' | '/'
}
+ def 'Create a node with observed-timestamp'() {
+ given: 'some json to create a data node'
+ def endpoint = "$dataNodeBaseEndpoint/anchors/$anchorName/nodes"
+ def json = 'some json (this is not validated)'
+ when: 'post is invoked with datanode endpoint and json'
+ def response =
+ mvc.perform(
+ post(endpoint)
+ .contentType(MediaType.APPLICATION_JSON)
+ .param('xpath', '')
+ .param('observed-timestamp', observedTimestamp)
+ .content(json)
+ ).andReturn().response
+ then: 'a created response is returned'
+ response.status == expectedHttpStatus.value()
+ then: 'the java API was called with the correct parameters'
+ expectedApiCount * mockCpsDataService.saveData(dataspaceName, anchorName, json,
+ { it == DateTimeUtility.toOffsetDateTime(observedTimestamp) })
+ where:
+ scenario | observedTimestamp || expectedApiCount | expectedHttpStatus
+ 'with observed-timestamp' | '2021-03-03T23:59:59.999-0400' || 1 | HttpStatus.CREATED
+ 'with invalid observed-timestamp' | 'invalid' || 0 | HttpStatus.BAD_REQUEST
+ }
+
def 'Create a child node'() {
given: 'some json to create a data node'
def endpoint = "$dataNodeBaseEndpoint/anchors/$anchorName/nodes"
@@ -104,34 +126,47 @@ class DataRestControllerSpec extends Specification {
and: 'parent node xpath'
def parentNodeXpath = 'some xpath'
when: 'post is invoked with datanode endpoint and json'
+ def postRequestBuilder = post(endpoint)
+ .contentType(MediaType.APPLICATION_JSON)
+ .param('xpath', parentNodeXpath)
+ .content(json)
+ if (observedTimestamp != null)
+ postRequestBuilder.param('observed-timestamp', observedTimestamp)
def response =
- mvc.perform(
- post(endpoint)
- .contentType(MediaType.APPLICATION_JSON)
- .param('xpath', parentNodeXpath)
- .content(json)
- ).andReturn().response
+ mvc.perform(postRequestBuilder).andReturn().response
then: 'a created response is returned'
response.status == HttpStatus.CREATED.value()
then: 'the java API was called with the correct parameters'
- 1 * mockCpsDataService.saveData(dataspaceName, anchorName, parentNodeXpath, json)
+ 1 * mockCpsDataService.saveData(dataspaceName, anchorName, parentNodeXpath, json,
+ DateTimeUtility.toOffsetDateTime(observedTimestamp))
+ where:
+ scenario | observedTimestamp
+ 'with observed-timestamp' | '2021-03-03T23:59:59.999-0400'
+ 'without observed-timestamp' | null
}
- def 'Create list node child elements.'() {
+ def 'Create list node child elements #scenario.'() {
given: 'parent node xpath and json data inputs'
def parentNodeXpath = 'parent node xpath'
def jsonData = 'json data'
when: 'post is invoked list-node endpoint'
- def response = mvc.perform(
- post("$dataNodeBaseEndpoint/anchors/$anchorName/list-node")
- .contentType(MediaType.APPLICATION_JSON)
- .param('xpath', parentNodeXpath)
- .content(jsonData)
- ).andReturn().response
+ def postRequestBuilder = post("$dataNodeBaseEndpoint/anchors/$anchorName/list-node")
+ .contentType(MediaType.APPLICATION_JSON)
+ .param('xpath', parentNodeXpath)
+ .content(jsonData)
+ if (observedTimestamp != null)
+ postRequestBuilder.param('observed-timestamp', observedTimestamp)
+ def response = mvc.perform(postRequestBuilder).andReturn().response
then: 'a created response is returned'
- response.status == HttpStatus.CREATED.value()
+ response.status == expectedHttpStatus.value()
then: 'the java API was called with the correct parameters'
- 1 * mockCpsDataService.saveListNodeData(dataspaceName, anchorName, parentNodeXpath, jsonData)
+ expectedApiCount * mockCpsDataService.saveListNodeData(dataspaceName, anchorName, parentNodeXpath, jsonData,
+ { it == DateTimeUtility.toOffsetDateTime(observedTimestamp) })
+ where:
+ scenario | observedTimestamp || expectedApiCount | expectedHttpStatus
+ 'with observed-timestamp' | '2021-03-03T23:59:59.999-0400' || 1 | HttpStatus.CREATED
+ 'without observed-timestamp' | null || 1 | HttpStatus.CREATED
+ 'with invalid observed-timestamp' | 'invalid' || 0 | HttpStatus.BAD_REQUEST
}
def 'Get data node with leaves'() {
@@ -141,8 +176,8 @@ class DataRestControllerSpec extends Specification {
mockCpsDataService.getDataNode(dataspaceName, anchorName, xpath, OMIT_DESCENDANTS) >> dataNodeWithLeavesNoChildren
when: 'get request is performed through REST API'
def response =
- mvc.perform(get(endpoint).param('xpath', xpath))
- .andReturn().response
+ mvc.perform(get(endpoint).param('xpath', xpath))
+ .andReturn().response
then: 'a success response is returned'
response.status == HttpStatus.OK.value()
and: 'response contains expected leaf and value'
@@ -158,11 +193,11 @@ class DataRestControllerSpec extends Specification {
mockCpsDataService.getDataNode(dataspaceName, anchorName, xpath, expectedCpsDataServiceOption) >> dataNode
when: 'get request is performed through REST API'
def response =
- mvc.perform(
- get(endpoint)
- .param('xpath', xpath)
- .param('include-descendants', includeDescendantsOption))
- .andReturn().response
+ mvc.perform(
+ get(endpoint)
+ .param('xpath', xpath)
+ .param('include-descendants', includeDescendantsOption))
+ .andReturn().response
then: 'a success response is returned'
response.status == HttpStatus.OK.value()
and: 'the response contains child is #expectChildInResponse'
@@ -180,14 +215,14 @@ class DataRestControllerSpec extends Specification {
def endpoint = "$dataNodeBaseEndpoint/anchors/$anchorName/nodes"
when: 'patch request is performed'
def response =
- mvc.perform(
- patch(endpoint)
- .contentType(MediaType.APPLICATION_JSON)
- .content(jsonData)
- .param('xpath', inputXpath)
- ).andReturn().response
+ mvc.perform(
+ patch(endpoint)
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(jsonData)
+ .param('xpath', inputXpath)
+ ).andReturn().response
then: 'the service method is invoked with expected parameters'
- 1 * mockCpsDataService.updateNodeLeaves(dataspaceName, anchorName, xpathServiceParameter, jsonData)
+ 1 * mockCpsDataService.updateNodeLeaves(dataspaceName, anchorName, xpathServiceParameter, jsonData, null)
and: 'response status indicates success'
response.status == HttpStatus.OK.value()
where:
@@ -197,20 +232,44 @@ class DataRestControllerSpec extends Specification {
'some xpath by parent' | '/some/xpath' || '/some/xpath'
}
+ def 'Update data node leaves with observedTimestamp'() {
+ given: 'json data'
+ def jsonData = 'json data'
+ def endpoint = "$dataNodeBaseEndpoint/anchors/$anchorName/nodes"
+ when: 'patch request is performed'
+ def response =
+ mvc.perform(
+ patch(endpoint)
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(jsonData)
+ .param('xpath', '/')
+ .param('observed-timestamp', observedTimestamp)
+ ).andReturn().response
+ then: 'the service method is invoked with expected parameters'
+ expectedApiCount * mockCpsDataService.updateNodeLeaves(dataspaceName, anchorName, '/', jsonData,
+ { it == DateTimeUtility.toOffsetDateTime(observedTimestamp) })
+ and: 'response status indicates success'
+ response.status == expectedHttpStatus.value()
+ where:
+ scenario | observedTimestamp || expectedApiCount | expectedHttpStatus
+ 'with observed-timestamp' | '2021-03-03T23:59:59.999-0400' || 1 | HttpStatus.OK
+ 'with invalid observed-timestamp' | 'invalid' || 0 | HttpStatus.BAD_REQUEST
+ }
+
def 'Replace data node tree: #scenario.'() {
given: 'json data'
def jsonData = 'json data'
def endpoint = "$dataNodeBaseEndpoint/anchors/$anchorName/nodes"
when: 'put request is performed'
def response =
- mvc.perform(
- put(endpoint)
- .contentType(MediaType.APPLICATION_JSON)
- .content(jsonData)
- .param('xpath', inputXpath))
- .andReturn().response
+ mvc.perform(
+ put(endpoint)
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(jsonData)
+ .param('xpath', inputXpath))
+ .andReturn().response
then: 'the service method is invoked with expected parameters'
- 1 * mockCpsDataService.replaceNodeTree(dataspaceName, anchorName, xpathServiceParameter, jsonData)
+ 1 * mockCpsDataService.replaceNodeTree(dataspaceName, anchorName, xpathServiceParameter, jsonData, noTimestamp)
and: 'response status indicates success'
response.status == HttpStatus.OK.value()
where:
@@ -220,34 +279,72 @@ class DataRestControllerSpec extends Specification {
'some xpath by parent' | '/some/xpath' || '/some/xpath'
}
+ def 'Replace data node tree with observedTimestamp.'() {
+ given: 'json data'
+ def jsonData = 'json data'
+ def endpoint = "$dataNodeBaseEndpoint/anchors/$anchorName/nodes"
+ when: 'put request is performed'
+ def response =
+ mvc.perform(
+ put(endpoint)
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(jsonData)
+ .param('xpath', '')
+ .param('observed-timestamp', observedTimestamp))
+ .andReturn().response
+ then: 'the service method is invoked with expected parameters'
+ expectedApiCount * mockCpsDataService.replaceNodeTree(dataspaceName, anchorName, '/', jsonData,
+ { it == DateTimeUtility.toOffsetDateTime(observedTimestamp) })
+ and: 'response status indicates success'
+ response.status == expectedHttpStatus.value()
+ where:
+ scenario | observedTimestamp || expectedApiCount | expectedHttpStatus
+ 'with observed-timestamp' | '2021-03-03T23:59:59.999-0400' || 1 | HttpStatus.OK
+ 'with invalid observed-timestamp' | 'invalid' || 0 | HttpStatus.BAD_REQUEST
+ }
+
def 'Replace list node child elements.'() {
given: 'parent node xpath and json data inputs'
def parentNodeXpath = 'parent node xpath'
def jsonData = 'json data'
when: 'patch is invoked list-node endpoint'
- def response = mvc.perform(
- patch("$dataNodeBaseEndpoint/anchors/$anchorName/list-node")
- .contentType(MediaType.APPLICATION_JSON)
- .param('xpath', parentNodeXpath)
- .content(jsonData)
- ).andReturn().response
+ def patchRequestBuilder = patch("$dataNodeBaseEndpoint/anchors/$anchorName/list-node")
+ .contentType(MediaType.APPLICATION_JSON)
+ .param('xpath', parentNodeXpath)
+ .content(jsonData)
+ if (observedTimestamp != null)
+ patchRequestBuilder.param('observed-timestamp', observedTimestamp)
+ def response = mvc.perform(patchRequestBuilder).andReturn().response
then: 'a success response is returned'
- response.status == HttpStatus.OK.value()
- then: 'the java API was called with the correct parameters'
- 1 * mockCpsDataService.replaceListNodeData(dataspaceName, anchorName, parentNodeXpath, jsonData)
+ response.status == expectedHttpStatus.value()
+ and: 'the java API was called with the correct parameters'
+ expectedApiCount * mockCpsDataService.replaceListNodeData(dataspaceName, anchorName, parentNodeXpath, jsonData,
+ { it == DateTimeUtility.toOffsetDateTime(observedTimestamp) })
+ where:
+ scenario | observedTimestamp || expectedApiCount | expectedHttpStatus
+ 'with observed-timestamp' | '2021-03-03T23:59:59.999-0400' || 1 | HttpStatus.OK
+ 'without observed-timestamp' | null || 1 | HttpStatus.OK
+ 'with invalid observed-timestamp' | 'invalid' || 0 | HttpStatus.BAD_REQUEST
}
- def 'Delete list node child elements.'() {
+ def 'Delete list node child elements. #scenario'() {
given: 'list node xpath'
def listNodeXpath = 'list node xpath'
when: 'delete is invoked list-node endpoint'
- def response = mvc.perform(
- delete("$dataNodeBaseEndpoint/anchors/$anchorName/list-node")
- .param('xpath', listNodeXpath)
- ).andReturn().response
+ def deleteRequestBuilder = delete("$dataNodeBaseEndpoint/anchors/$anchorName/list-node")
+ .param('xpath', listNodeXpath)
+ if (observedTimestamp != null)
+ deleteRequestBuilder.param('observed-timestamp', observedTimestamp)
+ def response = mvc.perform(deleteRequestBuilder).andReturn().response
then: 'a success response is returned'
- response.status == HttpStatus.NO_CONTENT.value()
- then: 'the java API was called with the correct parameters'
- 1 * mockCpsDataService.deleteListNodeData(dataspaceName, anchorName, listNodeXpath)
+ response.status == expectedHttpStatus.value()
+ and: 'the java API was called with the correct parameters'
+ expectedApiCount * mockCpsDataService.deleteListNodeData(dataspaceName, anchorName, listNodeXpath,
+ { it == DateTimeUtility.toOffsetDateTime(observedTimestamp) })
+ where:
+ scenario | observedTimestamp || expectedApiCount | expectedHttpStatus
+ 'with observed-timestamp' | '2021-03-03T23:59:59.999-0400' || 1 | HttpStatus.NO_CONTENT
+ 'without observed-timestamp' | null || 1 | HttpStatus.NO_CONTENT
+ 'with invalid observed-timestamp' | 'invalid' || 0 | HttpStatus.BAD_REQUEST
}
}
diff --git a/cps-rest/src/test/groovy/org/onap/cps/rest/exceptions/CpsRestExceptionHandlerSpec.groovy b/cps-rest/src/test/groovy/org/onap/cps/rest/exceptions/CpsRestExceptionHandlerSpec.groovy
index f44518d4b..079a59c66 100644
--- a/cps-rest/src/test/groovy/org/onap/cps/rest/exceptions/CpsRestExceptionHandlerSpec.groovy
+++ b/cps-rest/src/test/groovy/org/onap/cps/rest/exceptions/CpsRestExceptionHandlerSpec.groovy
@@ -111,7 +111,7 @@ class CpsRestExceptionHandlerSpec extends Specification {
def response = performTestRequest()
then: 'an HTTP Not Found response is returned with correct message and details'
assertTestResponse(response, NOT_FOUND, 'Object not found',
- 'Description does not exist in dataspace MyDataSpace.')
+ 'Description does not exist in dataspace MyDataSpace.')
}
def 'Request with an object already defined exception returns HTTP Status Conflict.'() {
@@ -120,8 +120,8 @@ class CpsRestExceptionHandlerSpec extends Specification {
def response = performTestRequest()
then: 'a HTTP conflict response is returned with correct message an details'
assertTestResponse(response, CONFLICT,
- "Already defined exception",
- "Anchor with name ${existingObjectName} already exists for ${dataspaceName}.")
+ "Already defined exception",
+ "Anchor with name ${existingObjectName} already exists for ${dataspaceName}.")
}
def 'Get request with a #exceptionThrown.class.simpleName returns HTTP Status Bad Request'() {
@@ -152,15 +152,16 @@ class CpsRestExceptionHandlerSpec extends Specification {
* NB. This method tests the expected behavior for POST request only;
* testing of PUT and PATCH requests omitted due to same NOT 'GET' condition is being used.
*/
+
def 'Post request with #exceptionThrown.class.simpleName returns HTTP Status Bad Request.'() {
given: '#exception is thrown the service indicating data is not found'
- mockCpsDataService.saveData(_, _, _, _) >> { throw exceptionThrown }
+ mockCpsDataService.saveData(_, _, _, _, _) >> { throw exceptionThrown }
when: 'data update request is performed'
def response = mvc.perform(
- post("$basePath/v1/dataspaces/dataspace-name/anchors/anchor-name/nodes")
- .contentType(MediaType.APPLICATION_JSON)
- .param('xpath', 'parent node xpath')
- .content('json data')
+ post("$basePath/v1/dataspaces/dataspace-name/anchors/anchor-name/nodes")
+ .contentType(MediaType.APPLICATION_JSON)
+ .param('xpath', 'parent node xpath')
+ .content('json data')
).andReturn().response
then: 'response code indicates bad input parameters'
response.status == BAD_REQUEST.value()
@@ -179,8 +180,8 @@ class CpsRestExceptionHandlerSpec extends Specification {
def performTestRequest() {
return mvc.perform(
- get("$basePath/v1/dataspaces/dataspace-name/anchors"))
- .andReturn().response
+ get("$basePath/v1/dataspaces/dataspace-name/anchors"))
+ .andReturn().response
}
static void assertTestResponse(response, expectedStatus, expectedErrorMessage, expectedErrorDetails) {
diff --git a/cps-rest/src/test/java/org/onap/cps/utils/DateTimeUtility.java b/cps-rest/src/test/java/org/onap/cps/utils/DateTimeUtility.java
new file mode 100644
index 000000000..f8d709647
--- /dev/null
+++ b/cps-rest/src/test/java/org/onap/cps/utils/DateTimeUtility.java
@@ -0,0 +1,40 @@
+/*
+ * ============LICENSE_START=======================================================
+ * Copyright (c) 2021 Bell Canada.
+ * ================================================================================
+ * 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.utils;
+
+import java.time.OffsetDateTime;
+import java.time.format.DateTimeFormatter;
+import org.springframework.util.StringUtils;
+
+public interface DateTimeUtility {
+
+ String ISO_TIMESTAMP_PATTERN = "yyyy-MM-dd'T'HH:mm:ss.SSSZ";
+ DateTimeFormatter ISO_TIMESTAMP_FORMATTER = DateTimeFormatter.ofPattern(ISO_TIMESTAMP_PATTERN);
+
+ static OffsetDateTime toOffsetDateTime(String datetTimestampAsString) {
+ return ! StringUtils.hasLength(datetTimestampAsString)
+ ? null : OffsetDateTime.parse(datetTimestampAsString, ISO_TIMESTAMP_FORMATTER);
+ }
+
+ static String toString(OffsetDateTime offsetDateTime) {
+ return offsetDateTime != null ? ISO_TIMESTAMP_FORMATTER.format(offsetDateTime) : null;
+ }
+}
diff --git a/cps-service/src/main/java/org/onap/cps/api/CpsDataService.java b/cps-service/src/main/java/org/onap/cps/api/CpsDataService.java
index 2583e9905..31a751734 100644
--- a/cps-service/src/main/java/org/onap/cps/api/CpsDataService.java
+++ b/cps-service/src/main/java/org/onap/cps/api/CpsDataService.java
@@ -2,6 +2,7 @@
* ============LICENSE_START=======================================================
* Copyright (C) 2020 Nordix Foundation
* Modifications Copyright (C) 2021 Pantheon.tech
+ * Modifications Copyright (C) 2021 Bell Canada
* ================================================================================
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -21,6 +22,7 @@
package org.onap.cps.api;
+import java.time.OffsetDateTime;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.onap.cps.spi.FetchDescendantsOption;
import org.onap.cps.spi.model.DataNode;
@@ -36,8 +38,10 @@ public interface CpsDataService {
* @param dataspaceName dataspace name
* @param anchorName anchor name
* @param jsonData json data
+ * @param observedTimestamp observedTimestamp
*/
- void saveData(@NonNull String dataspaceName, @NonNull String anchorName, @NonNull String jsonData);
+ void saveData(@NonNull String dataspaceName, @NonNull String anchorName, @NonNull String jsonData,
+ OffsetDateTime observedTimestamp);
/**
* Persists child data fragment under existing data node for the given anchor and dataspace.
@@ -46,21 +50,23 @@ public interface CpsDataService {
* @param anchorName anchor name
* @param parentNodeXpath parent node xpath
* @param jsonData json data
+ * @param observedTimestamp observedTimestamp
*/
void saveData(@NonNull String dataspaceName, @NonNull String anchorName, @NonNull String parentNodeXpath,
- @NonNull String jsonData);
+ @NonNull String jsonData, OffsetDateTime observedTimestamp);
/**
- * Persists child data fragment representing list-node (with one or more elements) under existing data node
- * for the given anchor and dataspace.
+ * Persists child data fragment representing list-node (with one or more elements) under existing data node for the
+ * given anchor and dataspace.
*
* @param dataspaceName dataspace name
* @param anchorName anchor name
* @param parentNodeXpath parent node xpath
* @param jsonData json data representing list element
+ * @param observedTimestamp observedTimestamp
*/
void saveListNodeData(@NonNull String dataspaceName, @NonNull String anchorName, @NonNull String parentNodeXpath,
- @NonNull String jsonData);
+ @NonNull String jsonData, OffsetDateTime observedTimestamp);
/**
* Retrieves datanode by XPath for given dataspace and anchor.
@@ -82,9 +88,10 @@ public interface CpsDataService {
* @param anchorName anchor name
* @param parentNodeXpath xpath to parent node
* @param jsonData json data
+ * @param observedTimestamp observedTimestamp
*/
void updateNodeLeaves(@NonNull String dataspaceName, @NonNull String anchorName, @NonNull String parentNodeXpath,
- @NonNull String jsonData);
+ @NonNull String jsonData, OffsetDateTime observedTimestamp);
/**
* Replaces existing data node content including descendants.
@@ -93,42 +100,47 @@ public interface CpsDataService {
* @param anchorName anchor name
* @param parentNodeXpath xpath to parent node
* @param jsonData json data
+ * @param observedTimestamp observedTimestamp
*/
void replaceNodeTree(@NonNull String dataspaceName, @NonNull String anchorName, @NonNull String parentNodeXpath,
- @NonNull String jsonData);
+ @NonNull String jsonData, OffsetDateTime observedTimestamp);
/**
- * Replaces (if exists) child data fragment representing list-node (with one or more elements)
- * under existing data node for the given anchor and dataspace.
+ * Replaces (if exists) child data fragment representing list-node (with one or more elements) under existing data
+ * node for the given anchor and dataspace.
*
- * @param dataspaceName dataspace name
- * @param anchorName anchor name
- * @param parentNodeXpath parent node xpath
- * @param jsonData json data representing list element
+ * @param dataspaceName dataspace name
+ * @param anchorName anchor name
+ * @param parentNodeXpath parent node xpath
+ * @param jsonData json data representing list element
+ * @param observedTimestamp observedTimestamp
*/
void replaceListNodeData(@NonNull String dataspaceName, @NonNull String anchorName, @NonNull String parentNodeXpath,
- @NonNull String jsonData);
+ @NonNull String jsonData, OffsetDateTime observedTimestamp);
/**
- * Deletes (if exists) child data fragment representing list-node (with one or more elements)
- * under existing data node for the given anchor and dataspace.
+ * Deletes (if exists) child data fragment representing list-node (with one or more elements) under existing data
+ * node for the given anchor and dataspace.
*
- * @param dataspaceName dataspace name
- * @param anchorName anchor name
- * @param listNodeXpath list node xpath
+ * @param dataspaceName dataspace name
+ * @param anchorName anchor name
+ * @param listNodeXpath list node xpath
+ * @param observedTimestamp observedTimestamp
*/
- void deleteListNodeData(@NonNull String dataspaceName, @NonNull String anchorName, @NonNull String listNodeXpath);
+ void deleteListNodeData(@NonNull String dataspaceName, @NonNull String anchorName, @NonNull String listNodeXpath,
+ OffsetDateTime observedTimestamp);
/**
- * Updates leaves of DataNode for given dataspace and anchor using xpath,
- * along with the leaves of each Child Data Node which already exists.
- * This method will throw an exception if data node update or any descendant update does not exist.
+ * Updates leaves of DataNode for given dataspace and anchor using xpath, along with the leaves of each Child Data
+ * Node which already exists. This method will throw an exception if data node update or any descendant update does
+ * not exist.
*
- * @param dataspaceName dataspace name
- * @param anchorName anchor name
- * @param parentNodeXpath xpath
+ * @param dataspaceName dataspace name
+ * @param anchorName anchor name
+ * @param parentNodeXpath xpath
* @param dataNodeUpdatesAsJson json data representing data node updates
+ * @param observedTimestamp observedTimestamp
*/
void updateNodeLeavesAndExistingDescendantLeaves(String dataspaceName, String anchorName, String parentNodeXpath,
- String dataNodeUpdatesAsJson);
+ String dataNodeUpdatesAsJson, OffsetDateTime observedTimestamp);
}
diff --git a/cps-service/src/main/java/org/onap/cps/api/impl/CpsDataServiceImpl.java b/cps-service/src/main/java/org/onap/cps/api/impl/CpsDataServiceImpl.java
index 8989dc80e..7b3567ed3 100755
--- a/cps-service/src/main/java/org/onap/cps/api/impl/CpsDataServiceImpl.java
+++ b/cps-service/src/main/java/org/onap/cps/api/impl/CpsDataServiceImpl.java
@@ -22,6 +22,7 @@
package org.onap.cps.api.impl;
+import java.time.OffsetDateTime;
import java.util.Collection;
import lombok.extern.slf4j.Slf4j;
import org.onap.cps.api.CpsAdminService;
@@ -61,27 +62,28 @@ public class CpsDataServiceImpl implements CpsDataService {
private NotificationService notificationService;
@Override
- public void saveData(final String dataspaceName, final String anchorName, final String jsonData) {
+ public void saveData(final String dataspaceName, final String anchorName, final String jsonData,
+ final OffsetDateTime observedTimestamp) {
final var dataNode = buildDataNodeFromJson(dataspaceName, anchorName, ROOT_NODE_XPATH, jsonData);
cpsDataPersistenceService.storeDataNode(dataspaceName, anchorName, dataNode);
- processDataUpdatedEventAsync(dataspaceName, anchorName);
+ processDataUpdatedEventAsync(dataspaceName, anchorName, observedTimestamp);
}
@Override
public void saveData(final String dataspaceName, final String anchorName, final String parentNodeXpath,
- final String jsonData) {
+ final String jsonData, final OffsetDateTime observedTimestamp) {
final var dataNode = buildDataNodeFromJson(dataspaceName, anchorName, parentNodeXpath, jsonData);
cpsDataPersistenceService.addChildDataNode(dataspaceName, anchorName, parentNodeXpath, dataNode);
- processDataUpdatedEventAsync(dataspaceName, anchorName);
+ processDataUpdatedEventAsync(dataspaceName, anchorName, observedTimestamp);
}
@Override
public void saveListNodeData(final String dataspaceName, final String anchorName,
- final String parentNodeXpath, final String jsonData) {
+ final String parentNodeXpath, final String jsonData, final OffsetDateTime observedTimestamp) {
final Collection<DataNode> dataNodesCollection =
buildDataNodeCollectionFromJson(dataspaceName, anchorName, parentNodeXpath, jsonData);
cpsDataPersistenceService.addListDataNodes(dataspaceName, anchorName, parentNodeXpath, dataNodesCollection);
- processDataUpdatedEventAsync(dataspaceName, anchorName);
+ processDataUpdatedEventAsync(dataspaceName, anchorName, observedTimestamp);
}
@Override
@@ -92,46 +94,48 @@ public class CpsDataServiceImpl implements CpsDataService {
@Override
public void updateNodeLeaves(final String dataspaceName, final String anchorName, final String parentNodeXpath,
- final String jsonData) {
+ final String jsonData, final OffsetDateTime observedTimestamp) {
final var dataNode = buildDataNodeFromJson(dataspaceName, anchorName, parentNodeXpath, jsonData);
cpsDataPersistenceService
.updateDataLeaves(dataspaceName, anchorName, dataNode.getXpath(), dataNode.getLeaves());
- processDataUpdatedEventAsync(dataspaceName, anchorName);
+ processDataUpdatedEventAsync(dataspaceName, anchorName, observedTimestamp);
}
@Override
public void updateNodeLeavesAndExistingDescendantLeaves(final String dataspaceName, final String anchorName,
- final String parentNodeXpath,
- final String dataNodeUpdatesAsJson) {
+ final String parentNodeXpath,
+ final String dataNodeUpdatesAsJson,
+ final OffsetDateTime observedTimestamp) {
final Collection<DataNode> dataNodeUpdates =
buildDataNodeCollectionFromJson(dataspaceName, anchorName, parentNodeXpath, dataNodeUpdatesAsJson);
for (final DataNode dataNodeUpdate : dataNodeUpdates) {
processDataNodeUpdate(dataspaceName, anchorName, dataNodeUpdate);
}
- notificationService.processDataUpdatedEvent(dataspaceName, anchorName);
+ processDataUpdatedEventAsync(dataspaceName, anchorName, observedTimestamp);
}
@Override
public void replaceNodeTree(final String dataspaceName, final String anchorName, final String parentNodeXpath,
- final String jsonData) {
+ final String jsonData, final OffsetDateTime observedTimestamp) {
final var dataNode = buildDataNodeFromJson(dataspaceName, anchorName, parentNodeXpath, jsonData);
cpsDataPersistenceService.replaceDataNodeTree(dataspaceName, anchorName, dataNode);
- processDataUpdatedEventAsync(dataspaceName, anchorName);
+ processDataUpdatedEventAsync(dataspaceName, anchorName, observedTimestamp);
}
@Override
public void replaceListNodeData(final String dataspaceName, final String anchorName, final String parentNodeXpath,
- final String jsonData) {
+ final String jsonData, final OffsetDateTime observedTimestamp) {
final Collection<DataNode> dataNodes =
buildDataNodeCollectionFromJson(dataspaceName, anchorName, parentNodeXpath, jsonData);
cpsDataPersistenceService.replaceListDataNodes(dataspaceName, anchorName, parentNodeXpath, dataNodes);
- processDataUpdatedEventAsync(dataspaceName, anchorName);
+ processDataUpdatedEventAsync(dataspaceName, anchorName, observedTimestamp);
}
@Override
- public void deleteListNodeData(final String dataspaceName, final String anchorName, final String listNodeXpath) {
+ public void deleteListNodeData(final String dataspaceName, final String anchorName, final String listNodeXpath,
+ final OffsetDateTime observedTimestamp) {
cpsDataPersistenceService.deleteListDataNodes(dataspaceName, anchorName, listNodeXpath);
- processDataUpdatedEventAsync(dataspaceName, anchorName);
+ processDataUpdatedEventAsync(dataspaceName, anchorName, observedTimestamp);
}
@@ -171,9 +175,10 @@ public class CpsDataServiceImpl implements CpsDataService {
}
- private void processDataUpdatedEventAsync(final String dataspaceName, final String anchorName) {
+ private void processDataUpdatedEventAsync(final String dataspaceName, final String anchorName,
+ final OffsetDateTime observedTimestamp) {
try {
- notificationService.processDataUpdatedEvent(dataspaceName, anchorName);
+ notificationService.processDataUpdatedEvent(dataspaceName, anchorName, observedTimestamp);
} catch (final Exception exception) {
log.error("Failed to send message to notification service", exception);
}
diff --git a/cps-service/src/main/java/org/onap/cps/notification/CpsDataUpdatedEventFactory.java b/cps-service/src/main/java/org/onap/cps/notification/CpsDataUpdatedEventFactory.java
index e0c8fe705..85e5abab0 100644
--- a/cps-service/src/main/java/org/onap/cps/notification/CpsDataUpdatedEventFactory.java
+++ b/cps-service/src/main/java/org/onap/cps/notification/CpsDataUpdatedEventFactory.java
@@ -42,7 +42,7 @@ public class CpsDataUpdatedEventFactory {
private static final URI EVENT_SCHEMA;
private static final URI EVENT_SOURCE;
private static final String EVENT_TYPE = "org.onap.cps.data-updated-event";
- private static final DateTimeFormatter dateTimeFormatter =
+ private static final DateTimeFormatter DATE_TIME_FORMATTER =
DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZ");
static {
@@ -64,22 +64,25 @@ public class CpsDataUpdatedEventFactory {
}
/**
- * Generates CPS Data Updated event.
+ * Generates CPS Data Updated event. If observedTimestamp is not provided, then current timestamp is used.
*
- * @param dataspaceName dataspaceName
- * @param anchorName anchorName
+ * @param dataspaceName dataspaceName
+ * @param anchorName anchorName
+ * @param observedTimestamp observedTimestamp
* @return CpsDataUpdatedEvent
*/
- public CpsDataUpdatedEvent createCpsDataUpdatedEvent(final String dataspaceName, final String anchorName) {
+ public CpsDataUpdatedEvent createCpsDataUpdatedEvent(final String dataspaceName, final String anchorName,
+ final OffsetDateTime observedTimestamp) {
final var dataNode = cpsDataService
.getDataNode(dataspaceName, anchorName, "/", FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS);
final var anchor = cpsAdminService.getAnchor(dataspaceName, anchorName);
- return toCpsDataUpdatedEvent(anchor, dataNode);
+ return toCpsDataUpdatedEvent(anchor, dataNode, observedTimestamp);
}
- private CpsDataUpdatedEvent toCpsDataUpdatedEvent(final Anchor anchor, final DataNode dataNode) {
+ private CpsDataUpdatedEvent toCpsDataUpdatedEvent(final Anchor anchor, final DataNode dataNode,
+ final OffsetDateTime observedTimestamp) {
final var cpsDataUpdatedEvent = new CpsDataUpdatedEvent();
- cpsDataUpdatedEvent.withContent(createContent(anchor, dataNode));
+ cpsDataUpdatedEvent.withContent(createContent(anchor, dataNode, observedTimestamp));
cpsDataUpdatedEvent.withId(UUID.randomUUID().toString());
cpsDataUpdatedEvent.withSchema(EVENT_SCHEMA);
cpsDataUpdatedEvent.withSource(EVENT_SOURCE);
@@ -93,13 +96,15 @@ public class CpsDataUpdatedEventFactory {
return data;
}
- private Content createContent(final Anchor anchor, final DataNode dataNode) {
+ private Content createContent(final Anchor anchor, final DataNode dataNode,
+ final OffsetDateTime observedTimestamp) {
final var content = new Content();
content.withAnchorName(anchor.getName());
content.withDataspaceName(anchor.getDataspaceName());
content.withSchemaSetName(anchor.getSchemaSetName());
content.withData(createData(dataNode));
- content.withObservedTimestamp(dateTimeFormatter.format(OffsetDateTime.now()));
+ content.withObservedTimestamp(
+ DATE_TIME_FORMATTER.format(observedTimestamp == null ? OffsetDateTime.now() : observedTimestamp));
return content;
}
}
diff --git a/cps-service/src/main/java/org/onap/cps/notification/NotificationService.java b/cps-service/src/main/java/org/onap/cps/notification/NotificationService.java
index 4745739a4..029efbe79 100644
--- a/cps-service/src/main/java/org/onap/cps/notification/NotificationService.java
+++ b/cps-service/src/main/java/org/onap/cps/notification/NotificationService.java
@@ -20,6 +20,7 @@
package org.onap.cps.notification;
+import java.time.OffsetDateTime;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
@@ -79,15 +80,17 @@ public class NotificationService {
*
* @param dataspaceName dataspace name
* @param anchorName anchor name
+ * @param observedTimestamp observedTimestamp
* @return future
*/
@Async("notificationExecutor")
- public Future<Void> processDataUpdatedEvent(final String dataspaceName, final String anchorName) {
+ public Future<Void> processDataUpdatedEvent(final String dataspaceName, final String anchorName,
+ final OffsetDateTime observedTimestamp) {
log.debug("process data updated event for dataspace '{}' & anchor '{}'", dataspaceName, anchorName);
try {
if (shouldSendNotification(dataspaceName)) {
final var cpsDataUpdatedEvent =
- cpsDataUpdatedEventFactory.createCpsDataUpdatedEvent(dataspaceName, anchorName);
+ cpsDataUpdatedEventFactory.createCpsDataUpdatedEvent(dataspaceName, anchorName, observedTimestamp);
log.debug("data updated event to be published {}", cpsDataUpdatedEvent);
notificationPublisher.sendNotification(cpsDataUpdatedEvent);
}
diff --git a/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsDataServiceImplSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsDataServiceImplSpec.groovy
index 97eac5aaa..6a0a4649a 100644
--- a/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsDataServiceImplSpec.groovy
+++ b/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsDataServiceImplSpec.groovy
@@ -22,6 +22,7 @@
package org.onap.cps.api.impl
+import java.time.OffsetDateTime
import org.onap.cps.TestUtils
import org.onap.cps.api.CpsAdminService
import org.onap.cps.api.CpsModuleService
@@ -55,18 +56,19 @@ class CpsDataServiceImplSpec extends Specification {
def dataspaceName = 'some dataspace'
def anchorName = 'some anchor'
def schemaSetName = 'some schema set'
+ def observedTimestamp = OffsetDateTime.now()
def 'Saving json data.'() {
given: 'schema set for given anchor and dataspace references test-tree model'
setupSchemaSetMocks('test-tree.yang')
when: 'save data method is invoked with test-tree json data'
def jsonData = TestUtils.getResourceFileContent('test-tree.json')
- objectUnderTest.saveData(dataspaceName, anchorName, jsonData)
+ objectUnderTest.saveData(dataspaceName, anchorName, jsonData, observedTimestamp)
then: 'the persistence service method is invoked with correct parameters'
1 * mockCpsDataPersistenceService.storeDataNode(dataspaceName, anchorName,
- { dataNode -> dataNode.xpath == '/test-tree' })
+ { dataNode -> dataNode.xpath == '/test-tree' })
and: 'data updated event is sent to notification service'
- 1 * mockNotificationService.processDataUpdatedEvent(dataspaceName, anchorName)
+ 1 * mockNotificationService.processDataUpdatedEvent(dataspaceName, anchorName, observedTimestamp)
}
def 'Saving child data fragment under existing node.'() {
@@ -74,12 +76,12 @@ class CpsDataServiceImplSpec extends Specification {
setupSchemaSetMocks('test-tree.yang')
when: 'save data method is invoked with test-tree json data'
def jsonData = '{"branch": [{"name": "New"}]}'
- objectUnderTest.saveData(dataspaceName, anchorName, '/test-tree', jsonData)
+ objectUnderTest.saveData(dataspaceName, anchorName, '/test-tree', jsonData, observedTimestamp)
then: 'the persistence service method is invoked with correct parameters'
1 * mockCpsDataPersistenceService.addChildDataNode(dataspaceName, anchorName, '/test-tree',
- { dataNode -> dataNode.xpath == '/test-tree/branch[@name=\'New\']' })
+ { dataNode -> dataNode.xpath == '/test-tree/branch[@name=\'New\']' })
and: 'data updated event is sent to notification service'
- 1 * mockNotificationService.processDataUpdatedEvent(dataspaceName, anchorName)
+ 1 * mockNotificationService.processDataUpdatedEvent(dataspaceName, anchorName, observedTimestamp)
}
def 'Saving list-node data fragment under existing node.'() {
@@ -87,19 +89,19 @@ class CpsDataServiceImplSpec extends Specification {
setupSchemaSetMocks('test-tree.yang')
when: 'save data method is invoked with list-node json data'
def jsonData = '{"branch": [{"name": "A"}, {"name": "B"}]}'
- objectUnderTest.saveListNodeData(dataspaceName, anchorName, '/test-tree', jsonData)
+ objectUnderTest.saveListNodeData(dataspaceName, anchorName, '/test-tree', jsonData, observedTimestamp)
then: 'the persistence service method is invoked with correct parameters'
1 * mockCpsDataPersistenceService.addListDataNodes(dataspaceName, anchorName, '/test-tree',
- { dataNodeCollection ->
- {
- assert dataNodeCollection.size() == 2
- assert dataNodeCollection.collect { it.getXpath() }
- .containsAll(['/test-tree/branch[@name=\'A\']', '/test-tree/branch[@name=\'B\']'])
- }
+ { dataNodeCollection ->
+ {
+ assert dataNodeCollection.size() == 2
+ assert dataNodeCollection.collect { it.getXpath() }
+ .containsAll(['/test-tree/branch[@name=\'A\']', '/test-tree/branch[@name=\'B\']'])
}
+ }
)
and: 'data updated event is sent to notification service'
- 1 * mockNotificationService.processDataUpdatedEvent(dataspaceName, anchorName)
+ 1 * mockNotificationService.processDataUpdatedEvent(dataspaceName, anchorName, observedTimestamp)
}
def 'Saving empty list-node data fragment.'() {
@@ -107,7 +109,7 @@ class CpsDataServiceImplSpec extends Specification {
setupSchemaSetMocks('test-tree.yang')
when: 'save data method is invoked with empty list-node data fragment'
def jsonData = '{"branch": []}'
- objectUnderTest.saveListNodeData(dataspaceName, anchorName, '/test-tree', jsonData)
+ objectUnderTest.saveListNodeData(dataspaceName, anchorName, '/test-tree', jsonData, observedTimestamp)
then: 'invalid data exception is thrown'
thrown(DataValidationException)
}
@@ -127,11 +129,11 @@ class CpsDataServiceImplSpec extends Specification {
given: 'schema set for given anchor and dataspace references test-tree model'
setupSchemaSetMocks('test-tree.yang')
when: 'update data method is invoked with json data #jsonData and parent node xpath #parentNodeXpath'
- objectUnderTest.updateNodeLeaves(dataspaceName, anchorName, parentNodeXpath, jsonData)
+ objectUnderTest.updateNodeLeaves(dataspaceName, anchorName, parentNodeXpath, jsonData, observedTimestamp)
then: 'the persistence service method is invoked with correct parameters'
1 * mockCpsDataPersistenceService.updateDataLeaves(dataspaceName, anchorName, expectedNodeXpath, leaves)
and: 'data updated event is sent to notification service'
- 1 * mockNotificationService.processDataUpdatedEvent(dataspaceName, anchorName)
+ 1 * mockNotificationService.processDataUpdatedEvent(dataspaceName, anchorName, observedTimestamp)
where: 'following parameters were used'
scenario | parentNodeXpath | jsonData || expectedNodeXpath | leaves
'top level node' | '/' | '{"test-tree": {"branch": []}}' || '/test-tree' | Collections.emptyMap()
@@ -142,7 +144,8 @@ class CpsDataServiceImplSpec extends Specification {
given: 'schema set for given anchor and dataspace references bookstore model'
setupSchemaSetMocks('bookstore.yang')
when: 'update data method is invoked with json data #jsonData and parent node xpath'
- objectUnderTest.updateNodeLeaves(dataspaceName, anchorName, '/bookstore/categories[@code=2]', jsonData)
+ objectUnderTest.updateNodeLeaves(dataspaceName, anchorName, '/bookstore/categories[@code=2]',
+ jsonData, observedTimestamp)
then: 'the persistence service method is invoked with correct parameters'
thrown(DataValidationException)
where: 'following parameters were used'
@@ -157,23 +160,25 @@ class CpsDataServiceImplSpec extends Specification {
and: 'the expected json string'
def jsonData = '{"cm-handles":[{"id":"cmHandle001", "additional-properties":[{"name":"P1"}]}]}'
when: 'update data method is invoked with json data and parent node xpath'
- objectUnderTest.updateNodeLeavesAndExistingDescendantLeaves(dataspaceName, anchorName, '/dmi-registry', jsonData)
+ objectUnderTest.updateNodeLeavesAndExistingDescendantLeaves(dataspaceName, anchorName,
+ '/dmi-registry', jsonData, observedTimestamp)
then: 'the persistence service method is invoked with correct parameters'
- 1 * mockCpsDataPersistenceService.updateDataLeaves(dataspaceName, anchorName, "/dmi-registry/cm-handles[@id='cmHandle001']", ['id': 'cmHandle001'])
+ 1 * mockCpsDataPersistenceService.updateDataLeaves(dataspaceName, anchorName,
+ "/dmi-registry/cm-handles[@id='cmHandle001']", ['id': 'cmHandle001'])
and: 'the data updated event is sent to the notification service'
- 1 * mockNotificationService.processDataUpdatedEvent(dataspaceName, anchorName)
+ 1 * mockNotificationService.processDataUpdatedEvent(dataspaceName, anchorName, observedTimestamp)
}
def 'Replace data node: #scenario.'() {
given: 'schema set for given anchor and dataspace references test-tree model'
setupSchemaSetMocks('test-tree.yang')
when: 'replace data method is invoked with json data #jsonData and parent node xpath #parentNodeXpath'
- objectUnderTest.replaceNodeTree(dataspaceName, anchorName, parentNodeXpath, jsonData)
+ objectUnderTest.replaceNodeTree(dataspaceName, anchorName, parentNodeXpath, jsonData, observedTimestamp)
then: 'the persistence service method is invoked with correct parameters'
1 * mockCpsDataPersistenceService.replaceDataNodeTree(dataspaceName, anchorName,
- { dataNode -> dataNode.xpath == expectedNodeXpath })
+ { dataNode -> dataNode.xpath == expectedNodeXpath })
and: 'data updated event is sent to notification service'
- 1 * mockNotificationService.processDataUpdatedEvent(dataspaceName, anchorName)
+ 1 * mockNotificationService.processDataUpdatedEvent(dataspaceName, anchorName, observedTimestamp)
where: 'following parameters were used'
scenario | parentNodeXpath | jsonData || expectedNodeXpath
'top level node' | '/' | '{"test-tree": {"branch": []}}' || '/test-tree'
@@ -185,19 +190,19 @@ class CpsDataServiceImplSpec extends Specification {
setupSchemaSetMocks('test-tree.yang')
when: 'replace list data method is invoked with list-node json data'
def jsonData = '{"branch": [{"name": "A"}, {"name": "B"}]}'
- objectUnderTest.replaceListNodeData(dataspaceName, anchorName, '/test-tree', jsonData)
+ objectUnderTest.replaceListNodeData(dataspaceName, anchorName, '/test-tree', jsonData, observedTimestamp)
then: 'the persistence service method is invoked with correct parameters'
1 * mockCpsDataPersistenceService.replaceListDataNodes(dataspaceName, anchorName, '/test-tree',
- { dataNodeCollection ->
- {
- assert dataNodeCollection.size() == 2
- assert dataNodeCollection.collect { it.getXpath() }
- .containsAll(['/test-tree/branch[@name=\'A\']', '/test-tree/branch[@name=\'B\']'])
- }
+ { dataNodeCollection ->
+ {
+ assert dataNodeCollection.size() == 2
+ assert dataNodeCollection.collect { it.getXpath() }
+ .containsAll(['/test-tree/branch[@name=\'A\']', '/test-tree/branch[@name=\'B\']'])
}
+ }
)
and: 'data updated event is sent to notification service'
- 1 * mockNotificationService.processDataUpdatedEvent(dataspaceName, anchorName)
+ 1 * mockNotificationService.processDataUpdatedEvent(dataspaceName, anchorName, observedTimestamp)
}
def 'Replace with empty list-node data fragment.'() {
@@ -205,7 +210,7 @@ class CpsDataServiceImplSpec extends Specification {
setupSchemaSetMocks('test-tree.yang')
when: 'replace list data method is invoked with empty list-node data fragment'
def jsonData = '{"branch": []}'
- objectUnderTest.replaceListNodeData(dataspaceName, anchorName, '/test-tree', jsonData)
+ objectUnderTest.replaceListNodeData(dataspaceName, anchorName, '/test-tree', jsonData, observedTimestamp)
then: 'invalid data exception is thrown'
thrown(DataValidationException)
}
@@ -214,11 +219,11 @@ class CpsDataServiceImplSpec extends Specification {
given: 'schema set for given anchor and dataspace references test-tree model'
setupSchemaSetMocks('test-tree.yang')
when: 'delete list data method is invoked with list-node json data'
- objectUnderTest.deleteListNodeData(dataspaceName, anchorName, '/test-tree/branch')
+ objectUnderTest.deleteListNodeData(dataspaceName, anchorName, '/test-tree/branch', observedTimestamp)
then: 'the persistence service method is invoked with correct parameters'
1 * mockCpsDataPersistenceService.deleteListDataNodes(dataspaceName, anchorName, '/test-tree/branch')
and: 'data updated event is sent to notification service'
- 1 * mockNotificationService.processDataUpdatedEvent(dataspaceName, anchorName)
+ 1 * mockNotificationService.processDataUpdatedEvent(dataspaceName, anchorName, observedTimestamp)
}
def setupSchemaSetMocks(String... yangResources) {
diff --git a/cps-service/src/test/groovy/org/onap/cps/api/impl/E2ENetworkSliceSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/api/impl/E2ENetworkSliceSpec.groovy
index fec8b7fa7..eefa86e90 100755
--- a/cps-service/src/test/groovy/org/onap/cps/api/impl/E2ENetworkSliceSpec.groovy
+++ b/cps-service/src/test/groovy/org/onap/cps/api/impl/E2ENetworkSliceSpec.groovy
@@ -22,6 +22,7 @@
package org.onap.cps.api.impl
+import java.time.OffsetDateTime
import org.onap.cps.TestUtils
import org.onap.cps.api.CpsAdminService
import org.onap.cps.notification.NotificationService
@@ -44,6 +45,7 @@ class E2ENetworkSliceSpec extends Specification {
def dataspaceName = 'someDataspace'
def anchorName = 'someAnchor'
def schemaSetName = 'someSchemaSet'
+ def noTimestamp = null
def setup() {
cpsDataServiceImpl.cpsDataPersistenceService = mockDataStoreService
@@ -92,7 +94,7 @@ class E2ENetworkSliceSpec extends Specification {
YangTextSchemaSourceSetBuilder.of(yangResourcesNameToContentMap)
mockModuleStoreService.getYangSchemaResources(dataspaceName, schemaSetName) >> schemaContext
when: 'saveData method is invoked'
- cpsDataServiceImpl.saveData(dataspaceName, anchorName, jsonData)
+ cpsDataServiceImpl.saveData(dataspaceName, anchorName, jsonData, noTimestamp)
then: 'Parameters are validated and processing is delegated to persistence service'
1 * mockDataStoreService.storeDataNode('someDataspace', 'someAnchor', _) >>
{ args -> dataNodeStored = args[2]}
@@ -124,7 +126,7 @@ class E2ENetworkSliceSpec extends Specification {
mockYangTextSchemaSourceSetCache.get('someDataspace', 'someSchemaSet') >> YangTextSchemaSourceSetBuilder.of(yangResourcesNameToContentMap)
mockModuleStoreService.getYangSchemaResources('someDataspace', 'someSchemaSet') >> schemaContext
when: 'saveData method is invoked'
- cpsDataServiceImpl.saveData('someDataspace', 'someAnchor', jsonData)
+ cpsDataServiceImpl.saveData('someDataspace', 'someAnchor', jsonData, noTimestamp)
then: 'parameters are validated and processing is delegated to persistence service'
1 * mockDataStoreService.storeDataNode('someDataspace', 'someAnchor', _) >>
{ args -> dataNodeStored = args[2]}
diff --git a/cps-service/src/test/groovy/org/onap/cps/notification/CpsDataUpdateEventFactorySpec.groovy b/cps-service/src/test/groovy/org/onap/cps/notification/CpsDataUpdateEventFactorySpec.groovy
index 2ce77bd1a..aa0c7c0b3 100644
--- a/cps-service/src/test/groovy/org/onap/cps/notification/CpsDataUpdateEventFactorySpec.groovy
+++ b/cps-service/src/test/groovy/org/onap/cps/notification/CpsDataUpdateEventFactorySpec.groovy
@@ -1,12 +1,13 @@
/*
* ============LICENSE_START=======================================================
- * Copyright (C) 2021 Bell Canada. All rights reserved.
+ * Copyright (c) 2021 Bell Canada.
* ================================================================================
* 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.
@@ -19,6 +20,9 @@
package org.onap.cps.notification
+import java.time.OffsetDateTime
+import java.time.format.DateTimeFormatter
+import org.onap.cps.utils.DateTimeUtility
import org.onap.cps.api.CpsAdminService
import org.onap.cps.api.CpsDataService
import org.onap.cps.event.model.Data
@@ -28,8 +32,6 @@ import org.onap.cps.spi.model.DataNodeBuilder
import org.springframework.util.StringUtils
import spock.lang.Specification
-import java.time.format.DateTimeFormatter
-
class CpsDataUpdateEventFactorySpec extends Specification {
def mockCpsDataService = Mock(CpsDataService)
@@ -42,7 +44,7 @@ class CpsDataUpdateEventFactorySpec extends Specification {
def mySchemasetName = 'my-schemaset-name'
def dateTimeFormat = 'yyyy-MM-dd\'T\'HH:mm:ss.SSSZ'
- def 'Create a CPS data updated event successfully.'() {
+ def 'Create a CPS data updated event successfully: #scenario'() {
given: 'cps admin service is able to return anchor details'
mockCpsAdminService.getAnchor(myDataspaceName, myAnchorName) >>
@@ -51,12 +53,14 @@ class CpsDataUpdateEventFactorySpec extends Specification {
def xpath = '/'
def dataNode = new DataNodeBuilder().withXpath(xpath).withLeaves(['leafName': 'leafValue']).build()
mockCpsDataService.getDataNode(
- myDataspaceName, myAnchorName, xpath, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> dataNode
+ myDataspaceName, myAnchorName, xpath, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> dataNode
when: 'CPS data updated event is created'
- def cpsDataUpdatedEvent = objectUnderTest.createCpsDataUpdatedEvent(myDataspaceName, myAnchorName)
+ def cpsDataUpdatedEvent = objectUnderTest.createCpsDataUpdatedEvent(myDataspaceName,
+ myAnchorName, DateTimeUtility.toOffsetDateTime(inputObservedTimestamp))
+
+ then: 'CPS data updated event is created with correct envelope'
- then: 'CPS data updated event is created with expected values'
with(cpsDataUpdatedEvent) {
type == 'org.onap.cps.data-updated-event'
source == new URI('urn:cps:org.onap.cps')
@@ -64,13 +68,24 @@ class CpsDataUpdateEventFactorySpec extends Specification {
StringUtils.hasText(id)
content != null
}
+ and: 'correct content'
with(cpsDataUpdatedEvent.content) {
assert isExpectedDateTimeFormat(observedTimestamp): "$observedTimestamp is not in $dateTimeFormat format"
- anchorName == myAnchorName
- dataspaceName == myDataspaceName
- schemaSetName == mySchemasetName
- data == new Data().withAdditionalProperty('leafName', 'leafValue')
+ if (inputObservedTimestamp != null)
+ assert observedTimestamp == inputObservedTimestamp
+ else
+ assert OffsetDateTime.now().minusSeconds(20).isBefore(
+ DateTimeUtility.toOffsetDateTime(observedTimestamp))
+ assert anchorName == myAnchorName
+ assert dataspaceName == myDataspaceName
+ assert schemaSetName == mySchemasetName
+ assert data == new Data().withAdditionalProperty('leafName', 'leafValue')
}
+ where:
+ scenario | inputObservedTimestamp
+ 'with observed timestamp -0400' | '2021-01-01T23:00:00.345-0400'
+ 'with observed timestamp +0400' | '2021-01-01T23:00:00.345+0400'
+ 'missing observed timestamp' | null
}
def isExpectedDateTimeFormat(String observedTimestamp) {
diff --git a/cps-service/src/test/groovy/org/onap/cps/notification/NotificationServiceSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/notification/NotificationServiceSpec.groovy
index ab727671e..875113d22 100644
--- a/cps-service/src/test/groovy/org/onap/cps/notification/NotificationServiceSpec.groovy
+++ b/cps-service/src/test/groovy/org/onap/cps/notification/NotificationServiceSpec.groovy
@@ -20,6 +20,7 @@
package org.onap.cps.notification
+import java.time.OffsetDateTime
import org.onap.cps.config.AsyncConfig
import org.onap.cps.event.model.CpsDataUpdatedEvent
import org.spockframework.spring.SpringBean
@@ -55,12 +56,13 @@ class NotificationServiceSpec extends Specification {
@Shared
def myDataspacePublishedName = 'my-dataspace-published'
def myAnchorName = 'my-anchorname'
+ def myObservedTimestamp = OffsetDateTime.now()
def 'Skip sending notification when disabled.'() {
given: 'notification is disabled'
spyNotificationProperties.isEnabled() >> false
when: 'dataUpdatedEvent is received'
- objectUnderTest.processDataUpdatedEvent(myDataspacePublishedName, myAnchorName)
+ objectUnderTest.processDataUpdatedEvent(myDataspacePublishedName, myAnchorName, myObservedTimestamp)
then: 'the notification is not sent'
0 * mockNotificationPublisher.sendNotification(_)
}
@@ -70,10 +72,11 @@ class NotificationServiceSpec extends Specification {
spyNotificationProperties.isEnabled() >> true
and: 'event factory can create event successfully'
def cpsDataUpdatedEvent = new CpsDataUpdatedEvent()
- mockCpsDataUpdatedEventFactory.createCpsDataUpdatedEvent(dataspaceName, myAnchorName) >> cpsDataUpdatedEvent
+ mockCpsDataUpdatedEventFactory.createCpsDataUpdatedEvent(dataspaceName, myAnchorName, myObservedTimestamp) >>
+ cpsDataUpdatedEvent
when: 'dataUpdatedEvent is received'
- def future = objectUnderTest.processDataUpdatedEvent(dataspaceName, myAnchorName)
- and: 'wait for async processing is completed'
+ def future = objectUnderTest.processDataUpdatedEvent(dataspaceName, myAnchorName, myObservedTimestamp)
+ and: 'wait for async processing to complete'
future.get(10, TimeUnit.SECONDS)
then: 'async process completed successfully'
future.isDone()
@@ -89,11 +92,11 @@ class NotificationServiceSpec extends Specification {
given: 'notification is enabled'
spyNotificationProperties.isEnabled() >> true
and: 'event factory can not create event successfully'
- mockCpsDataUpdatedEventFactory.createCpsDataUpdatedEvent(myDataspacePublishedName, myAnchorName) >>
+ mockCpsDataUpdatedEventFactory.createCpsDataUpdatedEvent(myDataspacePublishedName, myAnchorName, myObservedTimestamp) >>
{ throw new Exception("Could not create event") }
when: 'event is sent for processing'
- def future = objectUnderTest.processDataUpdatedEvent(myDataspacePublishedName, myAnchorName)
- and: 'wait for async processing is completed'
+ def future = objectUnderTest.processDataUpdatedEvent(myDataspacePublishedName, myAnchorName, myObservedTimestamp)
+ and: 'wait for async processing to complete'
future.get(10, TimeUnit.SECONDS)
then: 'async process completed successfully'
future.isDone()
diff --git a/cps-service/src/test/java/org/onap/cps/utils/DateTimeUtility.java b/cps-service/src/test/java/org/onap/cps/utils/DateTimeUtility.java
new file mode 100644
index 000000000..f8d709647
--- /dev/null
+++ b/cps-service/src/test/java/org/onap/cps/utils/DateTimeUtility.java
@@ -0,0 +1,40 @@
+/*
+ * ============LICENSE_START=======================================================
+ * Copyright (c) 2021 Bell Canada.
+ * ================================================================================
+ * 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.utils;
+
+import java.time.OffsetDateTime;
+import java.time.format.DateTimeFormatter;
+import org.springframework.util.StringUtils;
+
+public interface DateTimeUtility {
+
+ String ISO_TIMESTAMP_PATTERN = "yyyy-MM-dd'T'HH:mm:ss.SSSZ";
+ DateTimeFormatter ISO_TIMESTAMP_FORMATTER = DateTimeFormatter.ofPattern(ISO_TIMESTAMP_PATTERN);
+
+ static OffsetDateTime toOffsetDateTime(String datetTimestampAsString) {
+ return ! StringUtils.hasLength(datetTimestampAsString)
+ ? null : OffsetDateTime.parse(datetTimestampAsString, ISO_TIMESTAMP_FORMATTER);
+ }
+
+ static String toString(OffsetDateTime offsetDateTime) {
+ return offsetDateTime != null ? ISO_TIMESTAMP_FORMATTER.format(offsetDateTime) : null;
+ }
+}