Include Anchor and Xpath in equals and hashCode methods for
FragmentEntity. (This also requires adding equals and hashCode for
AnchorEntity and DataspaceEntity.) The combination of dataspace,
anchor, and xpath uniquely identify a fragment/datanode.
This allows FragmentEntity objects returned from query across anchors
to be stored in Set collections.
Performance was observed to be unaffected by the change.
Issue-ID: CPS-1664
Signed-off-by: danielhanrahan <daniel.hanrahan@est.tech>
Change-Id: I2c7e3957e392af36f5230d08c9bbd550f44e7444
/*
* ============LICENSE_START=======================================================
* Copyright (C) 2021 Pantheon.tech
/*
* ============LICENSE_START=======================================================
* Copyright (C) 2021 Pantheon.tech
+ * Modifications Copyright (C) 2023 Nordix Foundation.
* ================================================================================
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* ================================================================================
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
import javax.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Builder;
import javax.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Builder;
+import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Builder
@Entity
@Table(name = "anchor")
@Builder
@Entity
@Table(name = "anchor")
+@EqualsAndHashCode(onlyExplicitlyIncluded = true)
public class AnchorEntity implements Serializable {
private static final long serialVersionUID = -8049987915308262518L;
public class AnchorEntity implements Serializable {
private static final long serialVersionUID = -8049987915308262518L;
+ @EqualsAndHashCode.Include
private String name;
@NotNull
private String name;
@NotNull
@NotNull
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "dataspace_id")
@NotNull
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "dataspace_id")
+ @EqualsAndHashCode.Include
private DataspaceEntity dataspace;
}
private DataspaceEntity dataspace;
}
/*
* ============LICENSE_START=======================================================
/*
* ============LICENSE_START=======================================================
- * Copyright (C) 2020-2021 Nordix Foundation.
+ * Copyright (C) 2020-2023 Nordix Foundation.
* Modifications Copyright (C) 2020-2021 Pantheon.tech
* ================================================================================
* Licensed under the Apache License, Version 2.0 (the "License");
* Modifications Copyright (C) 2020-2021 Pantheon.tech
* ================================================================================
* Licensed under the Apache License, Version 2.0 (the "License");
import javax.persistence.Table;
import javax.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import javax.persistence.Table;
import javax.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
+import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@AllArgsConstructor
@NoArgsConstructor
@Table(name = "dataspace")
@AllArgsConstructor
@NoArgsConstructor
@Table(name = "dataspace")
+@EqualsAndHashCode(onlyExplicitlyIncluded = true)
public class DataspaceEntity implements Serializable {
private static final long serialVersionUID = 8395254649813051882L;
public class DataspaceEntity implements Serializable {
private static final long serialVersionUID = 8395254649813051882L;
@NotNull
@Column(columnDefinition = "text")
@NotNull
@Column(columnDefinition = "text")
+ @EqualsAndHashCode.Include
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "anchor_id")
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "anchor_id")
+ @EqualsAndHashCode.Include
private AnchorEntity anchor;
@ToString.Exclude
private AnchorEntity anchor;
@ToString.Exclude
def objectUnderTest = Spy(new CpsDataPersistenceServiceImpl(mockDataspaceRepository, mockAnchorRepository,
mockFragmentRepository, jsonObjectMapper, mockSessionManager))
def objectUnderTest = Spy(new CpsDataPersistenceServiceImpl(mockDataspaceRepository, mockAnchorRepository,
mockFragmentRepository, jsonObjectMapper, mockSessionManager))
- def anchorEntity = new AnchorEntity(id: 123, dataspace: new DataspaceEntity(id: 1))
+ static def anchorEntity = new AnchorEntity(id: 123, dataspace: new DataspaceEntity(id: 1))
def setup() {
mockAnchorRepository.getByDataspaceAndName(_, _) >> anchorEntity
def setup() {
mockAnchorRepository.getByDataspaceAndName(_, _) >> anchorEntity
where: 'the following Data Type is passed'
scenario | dataNodes || expectedFragmentEntities
'empty data node list' | [] || []
where: 'the following Data Type is passed'
scenario | dataNodes || expectedFragmentEntities
'empty data node list' | [] || []
- 'one data node in list' | [new DataNode(xpath: '/test/xpath', leaves: ['id': 'testId'], childDataNodes: [])] || [new FragmentEntity(xpath: '/test/xpath', attributes: '{"id":"testId"}', childFragments: [])]
+ 'one data node in list' | [new DataNode(xpath: '/test/xpath', leaves: ['id': 'testId'], childDataNodes: [])] || [new FragmentEntity(xpath: '/test/xpath', attributes: '{"id":"testId"}', anchor: anchorEntity, childFragments: [])]
}
def 'update data nodes and descendants'() {
given: 'the fragment repository returns fragment entities related to the xpath inputs'
}
def 'update data nodes and descendants'() {
given: 'the fragment repository returns fragment entities related to the xpath inputs'
- mockFragmentRepository.findExtractsWithDescendants(123, ['/test/xpath1', '/test/xpath2'] as Set, _) >> [
+ mockFragmentRepository.findExtractsWithDescendants(_, ['/test/xpath1', '/test/xpath2'] as Set, _) >> [
mockFragmentExtract(1, null, 123, '/test/xpath1', null),
mockFragmentExtract(2, null, 123, '/test/xpath2', null)
]
mockFragmentExtract(1, null, 123, '/test/xpath1', null),
mockFragmentExtract(2, null, 123, '/test/xpath2', null)
]
def dataNode1 = new DataNode(xpath: '/test/xpath1', leaves: ['id': 'testId1'], childDataNodes: [new DataNode(xpath: '/test/xpath1/child', leaves: ['id': 'childTestId1'])])
def dataNode2 = new DataNode(xpath: '/test/xpath2', leaves: ['id': 'testId2'], childDataNodes: [new DataNode(xpath: '/test/xpath2/child', leaves: ['id': 'childTestId2'])])
when: 'the fragment entities are update by the data nodes'
def dataNode1 = new DataNode(xpath: '/test/xpath1', leaves: ['id': 'testId1'], childDataNodes: [new DataNode(xpath: '/test/xpath1/child', leaves: ['id': 'childTestId1'])])
def dataNode2 = new DataNode(xpath: '/test/xpath2', leaves: ['id': 'testId2'], childDataNodes: [new DataNode(xpath: '/test/xpath2/child', leaves: ['id': 'childTestId2'])])
when: 'the fragment entities are update by the data nodes'
- objectUnderTest.updateDataNodesAndDescendants('dataspaceName', 'anchorName', [dataNode1, dataNode2])
+ objectUnderTest.updateDataNodesAndDescendants('dataspace', 'anchor', [dataNode1, dataNode2])
then: 'call fragment repository save all method is called with the updated fragments'
1 * mockFragmentRepository.saveAll({fragmentEntities -> {
then: 'call fragment repository save all method is called with the updated fragments'
1 * mockFragmentRepository.saveAll({fragmentEntities -> {
- fragmentEntities.containsAll([
- new FragmentEntity(xpath: '/test/xpath1', attributes: '{"id":"testId1"}', childFragments: [new FragmentEntity(xpath: '/test/xpath1/child', attributes: '{"id":"childTestId1"}', childFragments: [])]),
- new FragmentEntity(xpath: '/test/xpath2', attributes: '{"id":"testId2"}', childFragments: [new FragmentEntity(xpath: '/test/xpath2/child', attributes: '{"id":"childTestId2"}', childFragments: [])])
- ])
assert fragmentEntities.size() == 2
assert fragmentEntities.size() == 2
+ def fragmentEntityPerXpath = fragmentEntities.collectEntries { [it.xpath, it] }
+ assert fragmentEntityPerXpath.get('/test/xpath1').childFragments.first().attributes == '{"id":"childTestId1"}'
+ assert fragmentEntityPerXpath.get('/test/xpath2').childFragments.first().attributes == '{"id":"childTestId2"}'
def scenario = it.value
def dataNode = new DataNodeBuilder().withXpath(xpath).build()
dataNodes.add(dataNode)
def scenario = it.value
def dataNode = new DataNodeBuilder().withXpath(xpath).build()
dataNodes.add(dataNode)
- def fragmentExtract = mockFragmentExtract(fragmentId, null, null, xpath, null)
+ def fragmentExtract = mockFragmentExtract(fragmentId, null, 123, xpath, null)
fragmentExtracts.add(fragmentExtract)
fragmentExtracts.add(fragmentExtract)
- def fragmentEntity = new FragmentEntity(id: fragmentId, xpath: xpath, childFragments: [])
- mockFragmentRepository.getByDataspaceAndAnchorAndXpath(_, _, xpath) >> fragmentEntity
+ def fragmentEntity = new FragmentEntity(id: fragmentId, anchor: anchorEntity, xpath: xpath, childFragments: [])
if ('EXCEPTION' == scenario) {
mockFragmentRepository.save(fragmentEntity) >> { throw new StaleStateException("concurrent updates") }
}
if ('EXCEPTION' == scenario) {
mockFragmentRepository.save(fragmentEntity) >> { throw new StaleStateException("concurrent updates") }
}