Skip to content

Commit 19771e2

Browse files
NIAD-3050: Preserve UUIDs when mapping (#776)
* Update RandomIdGeneratorService to reuse UUID if UUID is provided Update and add additional tests * Update IdMapper to reuse UUID if UUID is provided Update and add additional tests * Update UnitTests to support new functionality * Update IdMapper for readability Update CHANGELOG.md
1 parent 80fb4eb commit 19771e2

18 files changed

+360
-84
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
66

77
## [Unreleased]
88

9+
### Fixed
10+
11+
* When mapping resources, if a UUID identifier is provided, this will be preserved in the produced XML. If a non-UUID identifier is provided, a new UUID will continue to be generated.
12+
913
## [2.0.4] - 2024-06-17
1014

1115
### Fixed
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,24 @@
11
package uk.nhs.adaptors.gp2gp.common.service;
22

33
import java.util.UUID;
4+
import java.util.regex.Pattern;
45

56
import org.springframework.stereotype.Service;
67

78
@Service
89
public class RandomIdGeneratorService {
10+
11+
private static final Pattern UUID_REGEX =
12+
Pattern.compile("^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$");
13+
914
public String createNewId() {
1015
return UUID.randomUUID().toString().toUpperCase();
1116
}
17+
18+
public String createNewOrUseExistingUUID(String id) {
19+
return UUID_REGEX.matcher(id).matches()
20+
? id.toUpperCase()
21+
: UUID.randomUUID().toString().toUpperCase();
22+
23+
}
1224
}

service/src/main/java/uk/nhs/adaptors/gp2gp/ehr/mapper/IdMapper.java

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,11 @@
2121
/**
2222
* There exists no requirement within GP Connect FHIR specification that the `IdType`
2323
* field is populated with a UUID.
24-
* The GP2GP HL7 specification however does mandate that DCE UUIDs are used within the
24+
* The GP2GP HL7 specification, however, does mandate that DCE UUIDs are used within the
2525
* Instance Identifier field.
26-
*
27-
* This class generates UUIDs for use within HL7, and maintains a mapping between FHIR
28-
* resource and UUID such that the same FHIR resource reference gets assigned the same
29-
* UUID.
26+
* This class generates UUIDs when a non-UUID id is provided, otherwise preserving the id, for use within HL7.
27+
* It also maintains a mapping between FHIR resource and UUID such that the same FHIR resource reference
28+
* gets assigned the same UUID.
3029
*/
3130
@Slf4j
3231
@AllArgsConstructor
@@ -39,23 +38,42 @@ public class IdMapper {
3938
ResourceType.PractitionerRole.name());
4039

4140
public String getOrNew(ResourceType resourceType, IdType id) {
42-
return getOrNew(buildReference(resourceType, id), true);
41+
return getOrNew(resourceType, id, true);
4342
}
4443

4544
public String newId(ResourceType unmappedResource, IdType id) {
46-
return getOrNew(buildReference(unmappedResource, id), false);
45+
return getOrNew(unmappedResource, id, false);
4746
}
4847

4948
public String getOrNew(Reference reference) {
50-
return getOrNew(reference, false);
49+
return getOrNewFromReference(reference);
5150
}
5251

53-
public String getOrNew(Reference reference, Boolean isResourceMapped) {
52+
public String getOrNewFromReference(Reference reference) {
53+
var referenceElement = reference.getReferenceElement();
54+
if (NOT_ALLOWED.contains(referenceElement.getResourceType())) {
55+
throw new EhrMapperException("Not allowed to use agent-related resource with IdMapper");
56+
}
57+
58+
var referenceId = referenceElement.getIdPart();
59+
var id = randomIdGeneratorService.createNewOrUseExistingUUID(referenceId);
60+
var defaultResourceId = new MappedId(id, false);
61+
var mappedId = ids.getOrDefault(reference.getReference(), defaultResourceId);
62+
63+
ids.put(reference.getReference(), mappedId);
64+
65+
return mappedId.getId();
66+
}
67+
68+
public String getOrNew(ResourceType resourceType, IdType id, Boolean isResourceMapped) {
69+
var reference = buildReference(resourceType, id);
5470
if (NOT_ALLOWED.contains(reference.getReferenceElement().getResourceType())) {
5571
throw new EhrMapperException("Not allowed to use agent-related resource with IdMapper");
5672
}
5773

58-
MappedId defaultResourceId = new MappedId(randomIdGeneratorService.createNewId(), isResourceMapped);
74+
var calculatedId = randomIdGeneratorService.createNewOrUseExistingUUID(id.getIdPart());
75+
76+
MappedId defaultResourceId = new MappedId(calculatedId, isResourceMapped);
5977
MappedId mappedId = ids.getOrDefault(reference.getReference(), defaultResourceId);
6078

6179
if (isResourceMapped) {
@@ -103,7 +121,7 @@ private static Reference buildReference(ResourceType resourceType, IdType id) {
103121
@Getter
104122
@Setter
105123
@AllArgsConstructor
106-
private static class MappedId {
124+
private static final class MappedId {
107125
private String id;
108126
private boolean isResourceMapped;
109127
}
Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package uk.nhs.adaptors.gp2gp.common.service;
22

33
import static org.assertj.core.api.Assertions.assertThat;
4-
import static org.assertj.core.api.Assertions.assertThatCode;
54
import static org.junit.jupiter.api.Assertions.assertAll;
65

76
import java.util.UUID;
@@ -13,16 +12,35 @@ public class RandomIdGeneratorServiceTest {
1312
private static final String UUID_UPPERCASE_REGEXP = "[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}";
1413

1514
@Test
16-
public void When_GeneratingRandomId_Expect_GeneratedIdIsRandomUUID() {
15+
public void When_CreatingNewId_Expect_GeneratedIdIsRandomUUID() {
1716
String id1 = new RandomIdGeneratorService().createNewId();
1817
String id2 = new RandomIdGeneratorService().createNewId();
1918

2019
assertAll(
21-
() -> assertThatCode(() -> UUID.fromString(id1)).doesNotThrowAnyException(),
22-
() -> assertThatCode(() -> UUID.fromString(id2)).doesNotThrowAnyException(),
2320
() -> assertThat(id1).isNotEqualTo(id2),
2421
() -> assertThat(id1).matches(UUID_UPPERCASE_REGEXP),
2522
() -> assertThat(id2).matches(UUID_UPPERCASE_REGEXP)
2623
);
2724
}
25+
26+
@Test
27+
public void When_GeneratingIdFromExistingId_And_IdIsAValidUUID_Expect_ThatUUIDIsUsed() {
28+
var uuidString = UUID.randomUUID().toString();
29+
30+
var generatedUUID = new RandomIdGeneratorService().createNewOrUseExistingUUID(uuidString);
31+
32+
assertThat(generatedUUID).isEqualTo(uuidString.toUpperCase());
33+
}
34+
35+
@Test
36+
public void When_GeneratingIdFromExistingId_And_IdIsNotAValidUUID_Expect_NewUUIDIsGenerated() {
37+
var idString = "THIS-IS-NOT-A-VALID-GUID";
38+
39+
var generatedUUID = new RandomIdGeneratorService().createNewOrUseExistingUUID(idString);
40+
41+
assertAll(
42+
() -> assertThat(generatedUUID).isNotEqualTo(idString),
43+
() -> assertThat(generatedUUID).matches(UUID_UPPERCASE_REGEXP)
44+
);
45+
}
2846
}

service/src/test/java/uk/nhs/adaptors/gp2gp/ehr/mapper/AllergyStructureMapperTest.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import static org.assertj.core.api.Assertions.assertThat;
44
import static org.junit.jupiter.api.Assertions.assertThrows;
55
import static org.mockito.ArgumentMatchers.any;
6+
import static org.mockito.ArgumentMatchers.anyString;
67
import static org.mockito.Mockito.lenient;
78
import static org.mockito.Mockito.when;
89

@@ -193,6 +194,7 @@ public void When_MappingAllergyIntoleranceJson_Expect_AllergyStructureXmlOutput(
193194
@BeforeEach
194195
public void setUp() throws IOException {
195196
when(randomIdGeneratorService.createNewId()).thenReturn(TEST_ID);
197+
when(randomIdGeneratorService.createNewOrUseExistingUUID(anyString())).thenReturn(TEST_ID);
196198

197199
lenient().when(codeableConceptCdMapper.mapToNullFlavorCodeableConcept(any(CodeableConcept.class)))
198200
.thenReturn(CodeableConceptMapperMockUtil.NULL_FLAVOR_CODE);
@@ -205,6 +207,7 @@ public void setUp() throws IOException {
205207
any(AllergyIntolerance.AllergyIntoleranceClinicalStatus.class)))
206208
.thenReturn(CodeableConceptMapperMockUtil.NULL_FLAVOR_CODE);
207209

210+
208211
var bundleInput = ResourceTestFileUtils.getFileContent(INPUT_JSON_BUNDLE);
209212
Bundle bundle = new FhirParseService().parseResource(bundleInput, Bundle.class);
210213
messageContext = new MessageContext(randomIdGeneratorService);

service/src/test/java/uk/nhs/adaptors/gp2gp/ehr/mapper/BloodPressureMapperTest.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
import static org.assertj.core.api.Assertions.assertThat;
2727
import static org.junit.jupiter.api.Assertions.assertThrows;
2828
import static org.mockito.ArgumentMatchers.any;
29+
import static org.mockito.ArgumentMatchers.anyString;
30+
import static org.mockito.Mockito.lenient;
2931
import static org.mockito.Mockito.when;
3032

3133
@ExtendWith(MockitoExtension.class)
@@ -68,7 +70,8 @@ public class BloodPressureMapperTest {
6870

6971
@BeforeEach
7072
public void setUp() {
71-
when(randomIdGeneratorService.createNewId()).thenReturn(TEST_ID);
73+
lenient().when(randomIdGeneratorService.createNewId()).thenReturn(TEST_ID);
74+
lenient().when(randomIdGeneratorService.createNewOrUseExistingUUID(anyString())).thenReturn(TEST_ID);
7275
messageContext = new MessageContext(randomIdGeneratorService);
7376
messageContext.initialize(new Bundle());
7477
bloodPressureMapper = new BloodPressureMapper(
@@ -142,6 +145,9 @@ private static Stream<Arguments> testArguments() {
142145

143146
@Test
144147
public void When_MappingBloodPressureWithCodeableConcepts_Expect_CompoundStatementXmlReturned() throws IOException {
148+
when(randomIdGeneratorService.createNewOrUseExistingUUID(any()))
149+
.thenReturn("5E496953-065B-41F2-9577-BE8F2FBD0757");
150+
145151
var jsonInput = ResourceTestFileUtils.getFileContent(BLOOD_PRESSURE_FILE_LOCATION + INPUT_BLOOD_PRESSURE_WITH_CODEABLE_CONCEPTS);
146152
var expectedOutput = ResourceTestFileUtils.getFileContent(
147153
BLOOD_PRESSURE_FILE_LOCATION + EXPECTED_BLOOD_PRESSURE_WITH_CODEABLE_CONCEPTS);

service/src/test/java/uk/nhs/adaptors/gp2gp/ehr/mapper/ConditionLinkSetMapperTest.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ public void setUp() throws IOException {
138138
lenient().when(messageContext.getIdMapper()).thenReturn(idMapper);
139139
lenient().when(messageContext.getAgentDirectory()).thenReturn(agentDirectory);
140140
lenient().when(messageContext.getInputBundleHolder()).thenReturn(inputBundle);
141-
lenient().when(randomIdGeneratorService.createNewId()).thenReturn(GENERATED_ID);
141+
142142
IdType conditionId = buildIdType(ResourceType.Condition, CONDITION_ID);
143143
IdType allergyId = buildIdType(ResourceType.AllergyIntolerance, ALLERGY_ID);
144144
IdType immunizationId = buildIdType(ResourceType.Immunization, IMMUNIZATION_ID);
@@ -147,6 +147,8 @@ public void setUp() throws IOException {
147147
lenient().when(idMapper.getOrNew(ResourceType.Observation, immunizationId)).thenReturn(IMMUNIZATION_ID);
148148
lenient().when(idMapper.getOrNew(any(Reference.class))).thenAnswer(answerWithObjectId(ResourceType.Condition));
149149
lenient().when(agentDirectory.getAgentId(any(Reference.class))).thenAnswer(answerWithObjectId());
150+
lenient().when(randomIdGeneratorService.createNewId()).thenReturn(GENERATED_ID);
151+
150152

151153
conditionLinkSetMapper = new ConditionLinkSetMapper(messageContext, randomIdGeneratorService, codeableConceptCdMapper,
152154
new ParticipantMapper());

service/src/test/java/uk/nhs/adaptors/gp2gp/ehr/mapper/DiaryPlanStatementMapperTest.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import static org.assertj.core.api.Assertions.assertThat;
44
import static org.junit.jupiter.api.Assertions.assertThrows;
55
import static org.mockito.ArgumentMatchers.any;
6+
import static org.mockito.ArgumentMatchers.anyString;
67
import static org.mockito.Mockito.when;
78

89
import java.io.IOException;
@@ -76,6 +77,7 @@ public void setUp() throws IOException {
7677
Bundle bundle = new FhirParseService().parseResource(inputJson, Bundle.class);
7778

7879
when(randomIdGeneratorService.createNewId()).thenReturn(TEST_ID);
80+
when(randomIdGeneratorService.createNewOrUseExistingUUID(anyString())).thenReturn(TEST_ID);
7981
when(codeableConceptCdMapper.mapCodeableConceptToCd(any(CodeableConcept.class)))
8082
.thenReturn(CodeableConceptMapperMockUtil.NULL_FLAVOR_CODE);
8183
messageContext = new MessageContext(randomIdGeneratorService);

service/src/test/java/uk/nhs/adaptors/gp2gp/ehr/mapper/DocumentReferenceToNarrativeStatementMapperTest.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424

2525
import static org.assertj.core.api.Assertions.assertThat;
2626
import static org.assertj.core.api.Assertions.assertThatThrownBy;
27+
import static org.mockito.ArgumentMatchers.anyString;
2728
import static org.mockito.Mockito.lenient;
2829

2930
@ExtendWith(MockitoExtension.class)
@@ -84,6 +85,8 @@ public class DocumentReferenceToNarrativeStatementMapperTest {
8485
@BeforeEach
8586
public void setUp() throws IOException {
8687
lenient().when(randomIdGeneratorService.createNewId()).thenReturn(TEST_ID);
88+
lenient().when(randomIdGeneratorService.createNewOrUseExistingUUID(anyString())).thenReturn(TEST_ID);
89+
8790
final String bundleInput = ResourceTestFileUtils.getFileContent(INPUT_JSON_BUNDLE);
8891
final Bundle bundle = new FhirParseService().parseResource(bundleInput, Bundle.class);
8992
messageContext = new MessageContext(randomIdGeneratorService);

service/src/test/java/uk/nhs/adaptors/gp2gp/ehr/mapper/EhrExtractMapperComponentTest.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@
3333

3434
import static org.assertj.core.api.Assertions.assertThat;
3535
import static org.mockito.ArgumentMatchers.any;
36+
import static org.mockito.ArgumentMatchers.anyString;
37+
import static org.mockito.Mockito.lenient;
3638
import static org.mockito.Mockito.when;
3739

3840
@ExtendWith(MockitoExtension.class)
@@ -105,6 +107,9 @@ public void setUp() {
105107
.build();
106108

107109
when(randomIdGeneratorService.createNewId()).thenReturn(TEST_ID_1, TEST_ID_2, TEST_ID_3);
110+
lenient().when(randomIdGeneratorService.createNewOrUseExistingUUID(anyString()))
111+
.thenReturn(TEST_ID_3);
112+
108113
when(timestampService.now()).thenReturn(Instant.parse(TEST_DATE_TIME));
109114
when(codeableConceptCdMapper.mapCodeableConceptToCd(any(CodeableConcept.class)))
110115
.thenReturn(CodeableConceptMapperMockUtil.NULL_FLAVOR_CODE);

0 commit comments

Comments
 (0)