# ============LICENSE_START=======================================================
# Copyright (c) 2021-2022 Bell Canada.
# Modifications Copyright (C) 2021-2022 Nordix Foundation
-# Modifications Copyright (C) 2022-2025 TechMahindra Ltd.
+# Modifications Copyright (C) 2022-2024 TechMahindra Ltd.
# Modifications Copyright (C) 2022 Deutsche Telekom AG
# ================================================================================
# Licensed under the Apache License, Version 2.0 (the "License");
responses:
'200':
$ref: 'components.yml#/components/responses/Ok'
- '201':
- $ref: 'components.yml#/components/responses/Created'
'400':
$ref: 'components.yml#/components/responses/BadRequest'
'403':
if (Boolean.TRUE.equals(dryRunEnabled)) {
cpsDataService.validateData(dataspaceName, anchorName, parentNodeXpath, nodeData, contentType);
return ResponseEntity.ok().build();
+ } else {
+ cpsDataService.updateDataNodeAndDescendants(dataspaceName, anchorName, parentNodeXpath,
+ nodeData, toOffsetDateTime(observedTimestamp), contentType);
}
- final boolean hasNewDataNodes = cpsDataService.updateDataNodeAndDescendants(dataspaceName, anchorName,
- parentNodeXpath, nodeData, toOffsetDateTime(observedTimestamp), contentType);
- final HttpStatus httpStatus = hasNewDataNodes ? HttpStatus.CREATED : HttpStatus.OK;
- return ResponseEntity.status(httpStatus).build();
+ return ResponseEntity.status(HttpStatus.OK).build();
}
@Override
'XML content: some xpath by parent' | '/some/xpath' | MediaType.APPLICATION_XML || '/some/xpath' | requestBodyXml | expectedXmlData | ContentType.XML
}
- def 'Replace data node tree returns #hasNewNodes for #scenario.'() {
- given: 'endpoint to replace node'
- def endpoint = "$dataNodeBaseEndpointV2/anchors/$anchorName/nodes"
- when: 'put request is performed'
- def response =
- mvc.perform(
- put(endpoint)
- .contentType(MediaType.APPLICATION_JSON)
- .content(requestBodyJson)
- .param('xpath', ''))
- .andReturn().response
- then: 'the cps data service method is invoked with expected parameters'
- 1 * mockCpsDataService.updateDataNodeAndDescendants(dataspaceName, anchorName, '/', expectedJsonData, noTimestamp, ContentType.JSON) >> hasNewNodes
- and: 'response status indicates success or creation'
- assert response.status == expectedStatus
- where:
- scenario | hasNewNodes || expectedStatus
- 'JSON content: root node updated only' | false || HttpStatus.OK.value()
- 'JSON content: root node with new list items' | true || HttpStatus.CREATED.value()
- }
-
def 'Validate data using Replace data node API.'() {
given: 'endpoint to replace node'
def endpoint = "$dataNodeBaseEndpointV1/anchors/$anchorName/nodes"
* Copyright (C) 2021-2025 OpenInfra Foundation Europe. All rights reserved.
* Modifications Copyright (C) 2021 Pantheon.tech
* Modifications Copyright (C) 2020-2022 Bell Canada.
- * Modifications Copyright (C) 2022-2025 TechMahindra Ltd.
+ * Modifications Copyright (C) 2022-2023 TechMahindra Ltd.
* ================================================================================
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
}
@Override
- public boolean updateDataNodesAndDescendants(final String dataspaceName, final String anchorName,
+ public void updateDataNodesAndDescendants(final String dataspaceName, final String anchorName,
final Collection<DataNode> updatedDataNodes) {
final AnchorEntity anchorEntity = getAnchorEntity(dataspaceName, anchorName);
+
final Map<String, DataNode> xpathToUpdatedDataNode = updatedDataNodes.stream()
.collect(Collectors.toMap(DataNode::getXpath, dataNode -> dataNode));
+
final Collection<String> xpaths = xpathToUpdatedDataNode.keySet();
Collection<FragmentEntity> existingFragmentEntities = getFragmentEntities(anchorEntity, xpaths);
- final List<DataNode> newDataNodes = identifyNewDataNodes(updatedDataNodes, existingFragmentEntities);
+
+ logMissingXPaths(xpaths, existingFragmentEntities);
+
existingFragmentEntities = fragmentRepository.prefetchDescendantsOfFragmentEntities(
FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS, existingFragmentEntities);
- updateExistingFragments(existingFragmentEntities, xpathToUpdatedDataNode);
- addFragmentEntitiesForNewDataNodes(anchorEntity, newDataNodes, existingFragmentEntities);
- persistFragmentEntitiesWithRetry(anchorEntity, existingFragmentEntities);
- return !newDataNodes.isEmpty();
- }
- private void updateExistingFragments(final Collection<FragmentEntity> existingFragmentEntities,
- final Map<String, DataNode> xpathToUpdatedDataNode) {
for (final FragmentEntity existingFragmentEntity : existingFragmentEntities) {
final DataNode updatedDataNode = xpathToUpdatedDataNode.get(existingFragmentEntity.getXpath());
updateFragmentEntityAndDescendantsWithDataNode(existingFragmentEntity, updatedDataNode);
}
- }
- private void addFragmentEntitiesForNewDataNodes(final AnchorEntity anchorEntity, final List<DataNode> newNodes,
- final Collection<FragmentEntity> existingFragmentEntities) {
- for (final DataNode newNode : newNodes) {
- final FragmentEntity newFragmentEntity = convertToFragmentWithAllDescendants(anchorEntity, newNode);
- existingFragmentEntities.add(newFragmentEntity);
- }
- }
-
- private void persistFragmentEntitiesWithRetry(final AnchorEntity anchorEntity,
- final Collection<FragmentEntity> existingFragmentEntities) {
try {
fragmentRepository.saveAll(existingFragmentEntities);
} catch (final StaleStateException staleStateException) {
}
}
- private List<DataNode> identifyNewDataNodes(final Collection<DataNode> updatedDataNodes,
- final Collection<FragmentEntity> existingFragmentEntities) {
- final Set<String> existingXpaths = existingFragmentEntities.stream()
- .map(FragmentEntity::getXpath)
- .collect(Collectors.toSet());
- return updatedDataNodes.stream()
- .filter(dataNode -> !existingXpaths.contains(dataNode.getXpath()))
- .collect(Collectors.toList());
- }
-
private void retryUpdateDataNodesIndividually(final AnchorEntity anchorEntity,
final Collection<FragmentEntity> fragmentEntities) {
final Collection<String> failedXpaths = new HashSet<>();
}
}
+ private static void logMissingXPaths(final Collection<String> xpaths,
+ final Collection<FragmentEntity> existingFragmentEntities) {
+ final Set<String> existingXPaths =
+ existingFragmentEntities.stream().map(FragmentEntity::getXpath).collect(Collectors.toSet());
+ final Set<String> missingXPaths =
+ xpaths.stream().filter(xpath -> !existingXPaths.contains(xpath)).collect(Collectors.toSet());
+ if (!missingXPaths.isEmpty()) {
+ log.warn("Cannot update data nodes: Target XPaths {} not found in DB.", missingXPaths);
+ }
+ }
}
* ============LICENSE_START=======================================================
* Copyright (c) 2021 Bell Canada.
* Modifications Copyright (C) 2021-2023 Nordix Foundation
- * Modifications Copyright (C) 2022-2025 TechMahindra Ltd.
+ * Modifications Copyright (C) 2022-2023 TechMahindra Ltd.
* ================================================================================
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
}})
}
- def 'Replace data nodes and add new list item'() {
- given: 'The fragment repository returns existing fragment for known XPath'
- def existingFragmentEntity = new FragmentEntity(id: 1, xpath: '/bookstore/categories[@code=1]/books[@title="Matilda"]', attributes: '{"title":"Matilda","lang":"English"}', anchor: anchorEntity, childFragments: [] as Set)
- mockFragmentRepository.findByAnchorAndXpathIn(_, _ as Set) >> [existingFragmentEntity]
- and: 'a data node that represents replacing an attribute from an existing one'
- def dataNode1 = new DataNode(xpath: '/bookstore/categories[@code=1]/books[@title="Matilda"]', leaves: ['title': 'Matilda', 'lang': 'French'])
- and: 'a data node that represents adding a new list item'
- def dataNode2 = new DataNode(xpath: '/bookstore/categories[@code=1]/books[@title="Book 2"]', leaves: ['title': 'Book 2', 'lang': 'English'])
- when: 'the fragment entities are updated by the data nodes'
- def result = objectUnderTest.updateDataNodesAndDescendants('dataspace', 'anchor', [dataNode1, dataNode2])
- then: 'fragment repository saves the updated new fragments'
- 1 * mockFragmentRepository.saveAll({ fragmentEntities ->
- {
- assert fragmentEntities.size() == 2
- def fragmentEntityPerXpath = fragmentEntities.collectEntries { [it.xpath, it] }
- assert fragmentEntityPerXpath.get('/bookstore/categories[@code=1]/books[@title="Matilda"]').attributes.contains('"lang":"French"')
- assert fragmentEntityPerXpath.get('/bookstore/categories[@code=1]/books[@title="Book 2"]').attributes.contains('"lang":"English"')
- }})
- and: 'result confirms new nodes were inserted'
- assert result
- }
-
def createDataNodeAndMockRepositoryMethodSupportingIt(xpath, scenario) {
def dataNode = new DataNodeBuilder().withXpath(xpath).build()
def fragmentEntity = new FragmentEntity(xpath: xpath, childFragments: [])
* @param nodeData node data
* @param observedTimestamp observedTimestamp
* @param contentType JSON/XML content type
- * @return true if new data nodes were created during the update;
- * false if only existing nodes were updated or no changes were applied
*/
- boolean updateDataNodeAndDescendants(String dataspaceName, String anchorName, String parentNodeXpath,
- String nodeData, OffsetDateTime observedTimestamp, ContentType contentType);
+ void updateDataNodeAndDescendants(String dataspaceName, String anchorName, String parentNodeXpath, String nodeData,
+ OffsetDateTime observedTimestamp, ContentType contentType);
/**
* Replaces multiple existing data nodes' content including descendants in a batch operation.
@Override
@Timed(value = "cps.data.service.datanode.descendants.update",
description = "Time taken to update a data node and descendants")
- public boolean updateDataNodeAndDescendants(final String dataspaceName, final String anchorName,
+ public void updateDataNodeAndDescendants(final String dataspaceName, final String anchorName,
final String parentNodeXpath, final String nodeData,
final OffsetDateTime observedTimestamp, final ContentType contentType) {
cpsValidator.validateNameCharacters(dataspaceName, anchorName);
final Anchor anchor = cpsAnchorService.getAnchor(dataspaceName, anchorName);
final Collection<DataNode> dataNodes = dataNodeFactory
.createDataNodesWithAnchorParentXpathAndNodeData(anchor, parentNodeXpath, nodeData, contentType);
- final boolean hasNewDataNodes = cpsDataPersistenceService
- .updateDataNodesAndDescendants(dataspaceName, anchorName, dataNodes);
+ cpsDataPersistenceService.updateDataNodesAndDescendants(dataspaceName, anchorName, dataNodes);
sendDataUpdatedEvent(anchor, parentNodeXpath, Operation.UPDATE, observedTimestamp);
- return hasNewDataNodes;
}
@Override
* Copyright (C) 2020-2025 OpenInfra Foundation Europe. All rights reserved.
* Modifications Copyright (C) 2021 Pantheon.tech
* Modifications Copyright (C) 2022 Bell Canada
- * Modifications Copyright (C) 2022-2025 TechMahindra Ltd.
+ * Modifications Copyright (C) 2022-2023 TechMahindra Ltd.
* ================================================================================
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* @param dataspaceName dataspace name
* @param anchorName anchor name
* @param dataNodes data nodes
- * @return true if new data nodes were created during the update;
- * false if only existing nodes were updated or no changes were applied
- *
*/
- boolean updateDataNodesAndDescendants(String dataspaceName, String anchorName, final Collection<DataNode>
- dataNodes);
+ void updateDataNodesAndDescendants(String dataspaceName, String anchorName, final Collection<DataNode> dataNodes);
/**
* Replaces list content by removing all existing elements and inserting the given new elements
schema:
type: object
description: OK
- "201":
- content:
- application/json:
- schema:
- example: my-resource
- type: string
- description: Created
"400":
content:
application/json: