- $ref: 'components.yml#/components/parameters/anchorNameInPath'
- $ref: 'components.yml#/components/parameters/requiredXpathInQuery'
- $ref: 'components.yml#/components/parameters/observedTimestampInQuery'
+ - $ref: 'components.yml#/components/parameters/contentTypeInHeader'
requestBody:
required: true
content:
application/json:
schema:
- type: object
+ type: string
examples:
dataSample:
$ref: 'components.yml#/components/examples/dataSample'
+ application/xml:
+ schema:
+ type: object
+ xml:
+ name: stores
+ examples:
+ dataSample:
+ $ref: 'components.yml#/components/examples/dataSampleXml'
responses:
'200':
$ref: 'components.yml#/components/responses/Ok'
@Override
public ResponseEntity<Object> replaceListContent(final String apiVersion, final String dataspaceName,
final String anchorName, final String parentNodeXpath,
- final Object jsonData, final String observedTimestamp) {
+ final String nodeData, final String observedTimestamp,
+ final String contentTypeInHeader) {
+ final ContentType contentType = ContentType.fromString(contentTypeInHeader);
cpsDataService.replaceListContent(dataspaceName, anchorName, parentNodeXpath,
- jsonObjectMapper.asJsonString(jsonData), toOffsetDateTime(observedTimestamp));
+ nodeData, toOffsetDateTime(observedTimestamp), contentType);
return new ResponseEntity<>(HttpStatus.OK);
}
return new ResponseEntity<>(jsonObjectMapper.asJsonString(deltaBetweenAnchors), HttpStatus.OK);
}
- ResponseEntity<Object> buildResponseEntity(final List<Map<String, Object>> dataMaps,
+ private ResponseEntity<Object> buildResponseEntity(final List<Map<String, Object>> dataMaps,
final ContentType contentType) {
final String responseData;
- if (contentType == ContentType.XML) {
+ if (ContentType.XML.equals(contentType)) {
responseData = XmlFileUtils.convertDataMapsToXml(dataMaps);
} else {
responseData = jsonObjectMapper.asJsonString(dataMaps);
private ResponseEntity<Object> buildResponseEntity(final List<Map<String, Object>> dataNodesAsListOfMaps,
final ContentType contentType) {
final String responseData;
- if (contentType == ContentType.XML) {
+ if (ContentType.XML.equals(contentType)) {
responseData = XmlFileUtils.convertDataMapsToXml(dataNodesAsListOfMaps);
} else {
responseData = jsonObjectMapper.asJsonString(dataNodesAsListOfMaps);
response.status == expectedHttpStatus.value()
and: 'the java API was called with the correct parameters'
expectedApiCount * mockCpsDataService.replaceListContent(dataspaceName, anchorName, 'parent xpath', expectedJsonData,
- { it == DateTimeUtility.toOffsetDateTime(observedTimestamp) })
+ { it == DateTimeUtility.toOffsetDateTime(observedTimestamp) }, ContentType.JSON)
+ 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 'Replace list XML content #scenario.'() {
+ when: 'list-nodes endpoint is invoked with put (update) operation'
+ def putRequestBuilder = put("$dataNodeBaseEndpointV1/anchors/$anchorName/list-nodes")
+ .contentType(MediaType.APPLICATION_XML)
+ .param('xpath', 'parent xpath')
+ .content(requestBodyXml)
+ if (observedTimestamp != null)
+ putRequestBuilder.param('observed-timestamp', observedTimestamp)
+ def response = mvc.perform(putRequestBuilder).andReturn().response
+ then: 'a success response is returned'
+ response.status == expectedHttpStatus.value()
+ and: 'the java API was called with the correct parameters'
+ expectedApiCount * mockCpsDataService.replaceListContent(dataspaceName, anchorName, 'parent xpath', expectedXmlData,
+ { it == DateTimeUtility.toOffsetDateTime(observedTimestamp) }, ContentType.XML)
where:
scenario | observedTimestamp || expectedApiCount | expectedHttpStatus
'with observed-timestamp' | '2021-03-03T23:59:59.999-0400' || 1 | HttpStatus.OK
* @param dataspaceName dataspace name
* @param anchorName anchor name
* @param parentNodeXpath parent node xpath
- * @param jsonData json data representing the new list elements
+ * @param nodeData node data representing the new list elements
* @param observedTimestamp observedTimestamp
+ * @param contentType JSON/XML content type
*/
- void replaceListContent(String dataspaceName, String anchorName, String parentNodeXpath, String jsonData,
- OffsetDateTime observedTimestamp);
+ void replaceListContent(String dataspaceName, String anchorName, String parentNodeXpath, String nodeData,
+ OffsetDateTime observedTimestamp, ContentType contentType);
/**
* Replaces list content by removing all existing elements and inserting the given new elements as data nodes
@Timed(value = "cps.data.service.list.update",
description = "Time taken to update a list")
public void replaceListContent(final String dataspaceName, final String anchorName, final String parentNodeXpath,
- final String jsonData, final OffsetDateTime observedTimestamp) {
+ final String nodeData, final OffsetDateTime observedTimestamp, final ContentType contentType) {
cpsValidator.validateNameCharacters(dataspaceName, anchorName);
final Anchor anchor = cpsAnchorService.getAnchor(dataspaceName, anchorName);
final Collection<DataNode> newListElements =
- buildDataNodesWithParentNodeXpath(anchor, parentNodeXpath, jsonData, ContentType.JSON);
+ buildDataNodesWithParentNodeXpath(anchor, parentNodeXpath, nodeData, contentType);
replaceListContent(dataspaceName, anchorName, parentNodeXpath, newListElements, observedTimestamp);
}
assert thrownUp == originalException
}
- def 'Replace list content data fragment under parent node.'() {
+ def 'Replace list content data fragment JSON under parent node.'() {
given: 'schema set for given anchor and dataspace references test-tree model'
setupSchemaSetMocks('test-tree.yang')
when: 'replace list data method is invoked with list element json data'
def jsonData = '{"branch": [{"name": "A"}, {"name": "B"}]}'
- objectUnderTest.replaceListContent(dataspaceName, anchorName, '/test-tree', jsonData, observedTimestamp)
+ objectUnderTest.replaceListContent(dataspaceName, anchorName, '/test-tree', jsonData, observedTimestamp, ContentType.JSON)
then: 'the persistence service method is invoked with correct parameters'
1 * mockCpsDataPersistenceService.replaceListContent(dataspaceName, anchorName, '/test-tree',
{ dataNodeCollection ->
2 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
}
+ def 'Replace list content data fragment XML under parent node.'() {
+ given: 'schema set for given anchor and dataspace references test-tree model'
+ setupSchemaSetMocks('test-tree.yang')
+ when: 'replace list data method is invoked with list element xml data'
+ def nodeData = '<branch><name>A</name></branch>'
+ objectUnderTest.replaceListContent(dataspaceName, anchorName, '/test-tree', nodeData, observedTimestamp, ContentType.XML)
+ then: 'the persistence service method is invoked with correct parameters'
+ 1 * mockCpsDataPersistenceService.replaceListContent(dataspaceName, anchorName, '/test-tree',
+ { dataNodeCollection ->
+ {
+ assert dataNodeCollection.size() == 1
+ assert dataNodeCollection.collect { it.getXpath() }
+ .containsAll(['/test-tree/branch[@name=\'A\']'])
+ }
+ }
+ )
+ and: 'the CpsValidator is called on the dataspaceName and AnchorName twice'
+ 2 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
+ }
+
def 'Replace whole list content with empty list element.'() {
given: 'schema set for given anchor and dataspace references test-tree model'
setupSchemaSetMocks('test-tree.yang')
when: 'replace list data method is invoked with empty list'
def jsonData = '{"branch": []}'
- objectUnderTest.replaceListContent(dataspaceName, anchorName, '/test-tree', jsonData, observedTimestamp)
+ objectUnderTest.replaceListContent(dataspaceName, anchorName, '/test-tree', jsonData, observedTimestamp, ContentType.JSON)
+ then: 'invalid data exception is thrown'
+ thrown(DataValidationException)
+ }
+
+ def 'Replace whole list content XML with empty list element.'() {
+ given: 'schema set for given anchor and dataspace references test-tree model'
+ setupSchemaSetMocks('test-tree.yang')
+ when: 'replace list data method is invoked with xml empty list'
+ def nodeData = '[]'
+ objectUnderTest.replaceListContent(dataspaceName, anchorName, '/test-tree', nodeData, observedTimestamp, ContentType.XML)
then: 'invalid data exception is thrown'
thrown(DataValidationException)
}
schema:
example: 2021-03-21T00:10:34.030-0100
type: string
+ - description: Content type in header
+ in: header
+ name: Content-Type
+ required: false
+ schema:
+ default: application/json
+ enum:
+ - application/json
+ - application/xml
+ type: string
requestBody:
content:
application/json:
dataSample:
$ref: '#/components/examples/dataSample'
value: null
+ schema:
+ type: string
+ application/xml:
+ examples:
+ dataSample:
+ $ref: '#/components/examples/dataSampleXml'
+ value: null
schema:
type: object
+ xml:
+ name: stores
required: true
responses:
"200":
def 'Attempt to add empty lists.'() {
when: 'the batches of new list element(s) are saved'
- objectUnderTest.replaceListContent(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1 , '/bookstore', [ ], now)
- then: 'an admin exception is thrown'
- thrown(CpsAdminException)
+ objectUnderTest.replaceListContent(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1 , '/bookstore', [ ] as String, now, ContentType.JSON)
+ then: 'an data exception is thrown'
+ thrown(DataValidationException)
}
def 'Add child error scenario: #scenario.'() {
assert countDataNodesInTree(objectUnderTest.getDataNodes(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, '/bookstore/categories[@code="2"]', DIRECT_CHILDREN_ONLY)) > 1
when: 'the categories list is replaced with just category "1" and without child nodes (books)'
def json = '{"categories": [ {"code":"' +categoryCode + '"' + childJson + '} ] }'
- objectUnderTest.replaceListContent(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, '/bookstore', json, now)
+ objectUnderTest.replaceListContent(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, '/bookstore', json, now, ContentType.JSON)
then: 'the new replaced category can be retrieved but has no children anymore'
assert expectedNumberOfDataNodes == countDataNodesInTree(objectUnderTest.getDataNodes(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, '/bookstore/categories[@code="' +categoryCode + '"]', DIRECT_CHILDREN_ONLY))
when: 'attempt to retrieve a category (code) not in the new list'
def jsonListData = generateJsonForOpenRoadmDevices(startId, totalNodes, changeLeaves)
when: 'the container node is updated'
resourceMeter.start()
- objectUnderTest.replaceListContent(CPS_PERFORMANCE_TEST_DATASPACE, UPDATE_TEST_ANCHOR, '/openroadm-devices', jsonListData, now)
+ objectUnderTest.replaceListContent(CPS_PERFORMANCE_TEST_DATASPACE, UPDATE_TEST_ANCHOR, '/openroadm-devices', jsonListData, now, ContentType.JSON)
resourceMeter.stop()
then: 'there are the expected number of total nodes'
assert totalNodes == countDataNodes('/openroadm-devices/openroadm-device')
def 'Update leaves for 100 data nodes.'() {
given: 'there are 200 existing data nodes'
def jsonListData = generateJsonForOpenRoadmDevices(1, 200, false)
- objectUnderTest.replaceListContent(CPS_PERFORMANCE_TEST_DATASPACE, UPDATE_TEST_ANCHOR, '/openroadm-devices', jsonListData, now)
+ objectUnderTest.replaceListContent(CPS_PERFORMANCE_TEST_DATASPACE, UPDATE_TEST_ANCHOR, '/openroadm-devices', jsonListData, now, ContentType.JSON)
and: 'JSON for updated data leaves of 100 nodes'
def jsonDataUpdated = "{'openroadm-device':[" + (1..100).collect {"{'device-id':'C201-7-1A-" + it + "','status':'fail','ne-state':'jeopardy'}" }.join(",") + "]}"
when: 'update is performed for leaves'