Skip to content

Commit 7abfd32

Browse files
NIAD-3070: Quantity encoding behaviour is wrong. (#793)
* Update functionality to correctly handle PQ Quantities in ObservationValueQuantityMapper Update Tests for these changes. Remove the source XML files no longer required * Update switch expression to if statements due to checkstyle defect * Update CHANGELOG.md * Update ObservationMapperTest, ObservationStatementMapperTest, BloodPressureMapperTest to ignore the whitespace changes in XML formatting for Quantities. Update ObservationValueQuantityMapper to ensure correct mapping of interval physical quantities Add Unit tests for uncertainty Update unit tests to check for any system or none. Update tests files for interval tests to reflect new mapping. * Update ObservationValueQuantityMapper to return an empty string and not generate a quantity if the value is not present in the valueQuantity Remove duplicate unit test * Update test name to conform with checkstyle * Remove no longer required tests xml and json files Add tests to cover all permutations of ObservationValueQuantityMapper Update ObservationValueQuantityMapper to properly handle system values * Add functionality to support UUID System values which are prefixed by `urn:uuid:` Update existing tests to support this change * Update ObservationValueQuantityMapper to inline templates rather than having them as constants * Update ObservationValueQuantityMapperTest to make test more readable
1 parent f7cd5ab commit 7abfd32

File tree

40 files changed

+703
-453
lines changed

40 files changed

+703
-453
lines changed

CHANGELOG.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88

99
### Added
1010

11-
* When mapping `ReferralRequest` supporting info and `DiagnosticReports`m NHS PMIP Report Numbers can also be provided
11+
* When mapping `ReferralRequest` supporting info and `DiagnosticReports` NHS PMIP Report Numbers can also be provided
1212
with the code system as a URN (`urn:oid:2.16.840.1.113883.2.1.4.5.5`).
1313

14+
### Fixed
15+
* When mapping physical quantities ("PQ") the produced XML now matches the HL7 XML specifications.
16+
1417
## [2.0.5] - 2024-07-04
1518

1619
### Fixed
Lines changed: 180 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,113 +1,195 @@
11
package uk.nhs.adaptors.gp2gp.ehr.mapper;
22

3-
import java.math.BigDecimal;
4-
53
import org.apache.commons.lang3.StringUtils;
64
import org.hl7.fhir.dstu3.model.BooleanType;
75
import org.hl7.fhir.dstu3.model.Extension;
86
import org.hl7.fhir.dstu3.model.Quantity;
97

10-
public final class ObservationValueQuantityMapper {
8+
import static org.hl7.fhir.dstu3.model.Quantity.QuantityComparator.GREATER_OR_EQUAL;
9+
import static org.hl7.fhir.dstu3.model.Quantity.QuantityComparator.LESS_OR_EQUAL;
10+
import static org.hl7.fhir.dstu3.model.Quantity.QuantityComparator.LESS_THAN;
1111

12-
private static final String UNITS_OF_MEASURE_SYSTEM = "http://unitsofmeasure.org";
13-
private static final String UNCERTAINTY_EXTENSION = "https://fhir.hl7.org.uk/STU3/StructureDefinition/Extension-CareConnect-ValueApproximation-1";
14-
private static final String IVL_PQ_ELEMENT = "<value xsi:type=\"IVL_PQ\">";
15-
16-
private static final String NO_COMPARATOR_VALUE_TEMPLATE = "<value xsi:type=\"PQ\" value=\"%s\" unit=\"%s\"/>";
17-
private static final String LESS_COMPARATOR_VALUE_TEMPLATE = IVL_PQ_ELEMENT
18-
+ "<high value=\"%s\" unit=\"%s\" inclusive=\"false\"/></value>";
19-
private static final String LESS_OR_EQUAL_COMPARATOR_VALUE_TEMPLATE = IVL_PQ_ELEMENT
20-
+ "<high value=\"%s\" unit=\"%s\" inclusive=\"true\"/></value>";
21-
private static final String GREATER_COMPARATOR_VALUE_TEMPLATE = IVL_PQ_ELEMENT + "<low value=\"%s\" "
22-
+ "unit=\"%s\" inclusive=\"false\"/></value>";
23-
private static final String GREATER_OR_EQUAL_COMPARATOR_VALUE_TEMPLATE = "<value xsi:type=\"IVL_PQ\"><low value=\"%s\" "
24-
+ "unit=\"%s\" inclusive=\"true\"/></value>";
25-
private static final String NO_COMPARATOR_NO_SYSTEM_VALUE_TEMPLATE = "<value xsi:type=\"PQ\" value=\"%s\" unit=\"1\">"
26-
+ "<translation value=\"%s\">%s</translation></value>";
27-
private static final String LESS_COMPARATOR_NO_SYSTEM_VALUE_TEMPLATE = IVL_PQ_ELEMENT
28-
+ "<high value=\"%s\" unit=\"1\" inclusive=\"false\"><translation value=\"%s\">"
29-
+ "%s</translation></high></value>";
30-
private static final String LESS_OR_EQUAL_COMPARATOR_NO_SYSTEM_VALUE_TEMPLATE = "<value xsi:type=\"IVL_PQ\"><high "
31-
+ "value=\"%s\" unit=\"1\" inclusive=\"true\"><translation value=\"%s\">"
32-
+ "%s</translation></high></value>";
33-
private static final String GREATER_COMPARATOR_NO_SYSTEM_VALUE_TEMPLATE = IVL_PQ_ELEMENT
34-
+ "<low value=\"%s\" unit=\"1\" inclusive=\"false\"><translation value=\"%s\">%s"
35-
+ "</translation></low></value>";
36-
private static final String GREATER_OR_EQUAL_COMPARATOR_NO_SYSTEM_VALUE_TEMPLATE = IVL_PQ_ELEMENT
37-
+ "<low value=\"%s\" unit=\"1\" inclusive=\"true\"><translation value=\"%s\">"
38-
+ "%s</translation></low></value>";
39-
private static final String UNCERTAINTY_CODE = "<uncertaintyCode code=\"U\" "
40-
+ "codeSystem=\"2.16.840.1.113883.5.1053\" displayName=\"Recorded as uncertain\"/>";
41-
private static final String QUANTITY_UNIT = "<originalText>%s</originalText>";
12+
public final class ObservationValueQuantityMapper {
4213

14+
private static final String UNITS_OF_MEASURE_SYSTEM =
15+
"http://unitsofmeasure.org";
16+
public static final String URN_OID_PREFIX =
17+
"urn:oid:";
18+
public static final String URN_UUID_PREFIX =
19+
"urn:uuid:";
20+
public static final String OID_REGEX =
21+
"(urn:oid:)?[0-2](\\.[1-9]\\d*)+";
22+
public static final String UUID_REGEX =
23+
"(urn:uuid:)?[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}$";
24+
25+
private static final String UNCERTAINTY_EXTENSION =
26+
"https://fhir.hl7.org.uk/STU3/StructureDefinition/Extension-CareConnect-ValueApproximation-1";
27+
private static final String UNCERTAINTY_CODE = """
28+
<uncertaintyCode code="U" codeSystem="2.16.840.1.113883.5.1053" displayName="Recorded as uncertain" />
29+
""";
4330

4431
private ObservationValueQuantityMapper() {
4532
}
4633

4734
public static String processQuantity(Quantity valueQuantity) {
48-
String result = StringUtils.EMPTY;
49-
if (isUncertaintyCodePresent(valueQuantity)) {
50-
result += UNCERTAINTY_CODE;
51-
}
52-
53-
if (!valueQuantity.hasComparator()) {
54-
result += prepareQuantityValueWithoutComparator(valueQuantity);
55-
} else {
56-
result += prepareQuantityValueAccordingToComparator(valueQuantity);
35+
if (!valueQuantity.hasValue()) {
36+
return StringUtils.EMPTY;
5737
}
5838

59-
return result;
60-
}
39+
var stringBuilder = new StringBuilder();
6140

62-
private static String prepareQuantityValueWithoutComparator(Quantity valueQuantity) {
63-
BigDecimal value = valueQuantity.getValue();
64-
if (valueQuantity.hasSystem() && valueQuantity.getSystem().equals(UNITS_OF_MEASURE_SYSTEM)) {
65-
return String.format(NO_COMPARATOR_VALUE_TEMPLATE, value, valueQuantity.getUnit());
66-
} else {
67-
return String.format(NO_COMPARATOR_NO_SYSTEM_VALUE_TEMPLATE, value, value, prepareUnit(valueQuantity));
41+
if (isUncertaintyCodePresent(valueQuantity)) {
42+
stringBuilder.append(UNCERTAINTY_CODE);
6843
}
69-
}
7044

71-
private static String prepareUnit(Quantity valueQuantity) {
72-
return valueQuantity.hasUnit() ? String.format(QUANTITY_UNIT, valueQuantity.getUnit()) : StringUtils.EMPTY;
45+
var quantityXml = valueQuantity.hasComparator()
46+
? getPhysicalQuantityIntervalXml(valueQuantity)
47+
: getPhysicalQuantityXml(valueQuantity);
48+
49+
return stringBuilder
50+
.append(quantityXml)
51+
.toString();
7352
}
7453

75-
private static String prepareQuantityValueAccordingToComparator(Quantity valueQuantity) {
76-
if (valueQuantity.getComparator() == Quantity.QuantityComparator.LESS_THAN) {
77-
return prepareQuantityValueByComparator(valueQuantity,
78-
LESS_COMPARATOR_VALUE_TEMPLATE,
79-
LESS_COMPARATOR_NO_SYSTEM_VALUE_TEMPLATE);
80-
} else if (valueQuantity.getComparator() == Quantity.QuantityComparator.LESS_OR_EQUAL) {
81-
return prepareQuantityValueByComparator(valueQuantity,
82-
LESS_OR_EQUAL_COMPARATOR_VALUE_TEMPLATE,
83-
LESS_OR_EQUAL_COMPARATOR_NO_SYSTEM_VALUE_TEMPLATE);
84-
} else if (valueQuantity.getComparator() == Quantity.QuantityComparator.GREATER_THAN) {
85-
return prepareQuantityValueByComparator(valueQuantity,
86-
GREATER_COMPARATOR_VALUE_TEMPLATE,
87-
GREATER_COMPARATOR_NO_SYSTEM_VALUE_TEMPLATE);
88-
} else if (valueQuantity.getComparator() == Quantity.QuantityComparator.GREATER_OR_EQUAL) {
89-
return prepareQuantityValueByComparator(valueQuantity,
90-
GREATER_OR_EQUAL_COMPARATOR_VALUE_TEMPLATE,
91-
GREATER_OR_EQUAL_COMPARATOR_NO_SYSTEM_VALUE_TEMPLATE);
54+
private static String getPhysicalQuantityXml(Quantity valueQuantity) {
55+
if (UNITS_OF_MEASURE_SYSTEM.equals(valueQuantity.getSystem()) && valueQuantity.hasCode()) {
56+
return """
57+
<value xsi:type="PQ" value="%s" unit="%s" />"""
58+
.formatted(
59+
valueQuantity.getValue(),
60+
valueQuantity.getCode()
61+
);
62+
}
63+
if (hasValidSystem(valueQuantity) && valueQuantity.hasCode() && valueQuantity.hasUnit()) {
64+
return """
65+
<value xsi:type="PQ" value="%s" unit="1">%n\
66+
<translation value="%s" code="%s" codeSystem="%s" displayName="%s" />%n\
67+
</value>"""
68+
.formatted(
69+
valueQuantity.getValue(),
70+
valueQuantity.getValue(),
71+
valueQuantity.getCode(),
72+
getSystemWithoutPrefix(valueQuantity.getSystem()),
73+
valueQuantity.getUnit()
74+
);
75+
}
76+
if (hasValidSystem(valueQuantity) && valueQuantity.hasCode()) {
77+
return """
78+
<value xsi:type="PQ" value="%s" unit="1">%n\
79+
<translation value="%s" code="%s" codeSystem="%s" />%n\
80+
</value>"""
81+
.formatted(
82+
valueQuantity.getValue(),
83+
valueQuantity.getValue(),
84+
valueQuantity.getCode(),
85+
getSystemWithoutPrefix(valueQuantity.getSystem())
86+
);
87+
}
88+
if (valueQuantity.hasUnit()) {
89+
return """
90+
<value xsi:type="PQ" value="%s" unit="1">%n\
91+
<translation value="%s">%n\
92+
<originalText>%s</originalText>%n\
93+
</translation>%n\
94+
</value>"""
95+
.formatted(
96+
valueQuantity.getValue(),
97+
valueQuantity.getValue(),
98+
valueQuantity.getUnit()
99+
);
92100
}
93101

94-
return StringUtils.EMPTY;
102+
return """
103+
<value xsi:type="PQ" value="%s" unit="1" />"""
104+
.formatted(valueQuantity.getValue());
95105
}
96106

97-
private static String prepareQuantityValueByComparator(Quantity valueQuantity, String systemTemplate, String nonSystemTemplate) {
98-
if (valueQuantity.hasSystem() && valueQuantity.getSystem().equals(UNITS_OF_MEASURE_SYSTEM)) {
99-
return formatSystemTemplate(systemTemplate, valueQuantity.getValue(), valueQuantity.getCode());
107+
private static String getPhysicalQuantityIntervalXml(Quantity valueQuantity) {
108+
if (UNITS_OF_MEASURE_SYSTEM.equals(valueQuantity.getSystem()) && valueQuantity.hasCode()) {
109+
return """
110+
<value xsi:type="IVL_PQ">%n\
111+
<%s value="%s" unit="%s" inclusive="%s" />%n\
112+
</value>"""
113+
.formatted(
114+
getHighOrLow(valueQuantity),
115+
valueQuantity.getValue(),
116+
valueQuantity.getCode(),
117+
isInclusive(valueQuantity)
118+
);
119+
}
120+
if (hasValidSystem(valueQuantity) && valueQuantity.hasCode() && valueQuantity.hasUnit()) {
121+
return """
122+
<value xsi:type="IVL_PQ">%n\
123+
<%s value="%s" unit="1" inclusive="%s">%n\
124+
<translation value="%s" code="%s" codeSystem="%s" displayName="%s" />%n\
125+
</%s>%n\
126+
</value>"""
127+
.formatted(
128+
getHighOrLow(valueQuantity),
129+
valueQuantity.getValue(),
130+
isInclusive(valueQuantity),
131+
valueQuantity.getValue(),
132+
valueQuantity.getCode(),
133+
getSystemWithoutPrefix(valueQuantity.getSystem()),
134+
valueQuantity.getUnit(),
135+
getHighOrLow(valueQuantity)
136+
);
137+
}
138+
if (hasValidSystem(valueQuantity) && valueQuantity.hasCode()) {
139+
return """
140+
<value xsi:type="IVL_PQ">%n\
141+
<%s value="%s" unit="1" inclusive="%s">%n\
142+
<translation value="%s" code="%s" codeSystem="%s" />%n\
143+
</%s>%n\
144+
</value>"""
145+
.formatted(
146+
getHighOrLow(valueQuantity),
147+
valueQuantity.getValue(),
148+
isInclusive(valueQuantity),
149+
valueQuantity.getValue(),
150+
valueQuantity.getCode(),
151+
getSystemWithoutPrefix(valueQuantity.getSystem()),
152+
getHighOrLow(valueQuantity)
153+
);
154+
}
155+
if (valueQuantity.hasUnit()) {
156+
return """
157+
<value xsi:type="IVL_PQ">%n\
158+
<%s value="%s" unit="1" inclusive="%s">%n\
159+
<translation value="%s">%n\
160+
<originalText>%s</originalText>%n\
161+
</translation>%n\
162+
</%s>%n\
163+
</value>"""
164+
.formatted(
165+
getHighOrLow(valueQuantity),
166+
valueQuantity.getValue(),
167+
isInclusive(valueQuantity),
168+
valueQuantity.getValue(),
169+
valueQuantity.getUnit(),
170+
getHighOrLow(valueQuantity)
171+
);
100172
}
101173

102-
return formatNoSystemTemplate(nonSystemTemplate, valueQuantity.getValue(), prepareUnit(valueQuantity));
174+
return """
175+
<value xsi:type="IVL_PQ">%n\
176+
<%s value="%s" unit="1" inclusive="%s" />%n\
177+
</value>"""
178+
.formatted(
179+
getHighOrLow(valueQuantity),
180+
valueQuantity.getValue(),
181+
isInclusive(valueQuantity)
182+
);
103183
}
104184

105-
private static String formatSystemTemplate(String template, BigDecimal value, String code) {
106-
return String.format(template, value, code);
185+
private static boolean isInclusive(Quantity valueQuantity) {
186+
return valueQuantity.getComparator() == GREATER_OR_EQUAL || valueQuantity.getComparator() == LESS_OR_EQUAL;
107187
}
108188

109-
private static String formatNoSystemTemplate(String template, BigDecimal value, String unit) {
110-
return String.format(template, value, value, unit);
189+
private static String getHighOrLow(Quantity valueQuantity) {
190+
return valueQuantity.getComparator() == LESS_THAN || valueQuantity.getComparator() == LESS_OR_EQUAL
191+
? "high"
192+
: "low";
111193
}
112194

113195
private static boolean isUncertaintyCodePresent(Quantity valueQuantity) {
@@ -118,7 +200,6 @@ private static boolean isUncertaintyCodePresent(Quantity valueQuantity) {
118200
}
119201
}
120202
}
121-
122203
return false;
123204
}
124205

@@ -128,4 +209,23 @@ private static boolean isUncertaintyExtension(Extension extension) {
128209
&& extension.getValue() instanceof BooleanType
129210
&& ((BooleanType) extension.getValue()).booleanValue();
130211
}
212+
213+
private static boolean hasValidSystem(Quantity valueQuantity) {
214+
return valueQuantity.hasSystem() && (
215+
valueQuantity.getSystem().equals(UNITS_OF_MEASURE_SYSTEM)
216+
|| valueQuantity.getSystem().matches(OID_REGEX)
217+
|| valueQuantity.getSystem().matches(UUID_REGEX)
218+
);
219+
}
220+
221+
private static String getSystemWithoutPrefix(String system) {
222+
if (system.startsWith(URN_OID_PREFIX)) {
223+
return StringUtils.removeStart(system, URN_OID_PREFIX);
224+
}
225+
if (system.startsWith(URN_UUID_PREFIX)) {
226+
return StringUtils.removeStart(system, URN_UUID_PREFIX);
227+
}
228+
229+
return system;
230+
}
131231
}

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ public void When_MappingBloodPressureWithNestedTrue_Expect_CompoundStatementXmlR
109109
Observation observation = new FhirParseService().parseResource(jsonInput, Observation.class);
110110
var outputMessage = bloodPressureMapper.mapBloodPressure(observation, true);
111111

112-
assertThat(outputMessage).isEqualTo(expectedOutput);
112+
assertThat(outputMessage).isEqualToIgnoringWhitespace(expectedOutput);
113113
}
114114

115115
@ParameterizedTest
@@ -126,7 +126,7 @@ public void When_MappingBloodPressure_Expect_CompoundStatementXmlReturned(String
126126

127127
assertThat(outputMessage)
128128
.describedAs(TestArgumentsLoaderUtil.FAIL_MESSAGE, inputJson, outputXml)
129-
.isEqualTo(expectedOutput);
129+
.isEqualToIgnoringWhitespace(expectedOutput);
130130
}
131131

132132
private static Stream<Arguments> testArguments() {
@@ -160,7 +160,7 @@ messageContext, randomIdGeneratorService, new StructuredObservationValueMapper()
160160
Observation observation = new FhirParseService().parseResource(jsonInput, Observation.class);
161161
var outputMessage = bloodPressureMapper.mapBloodPressure(observation, true);
162162

163-
assertThat(outputMessage).isEqualTo(expectedOutput);
163+
assertThat(outputMessage).isEqualToIgnoringWhitespace(expectedOutput);
164164
}
165165

166166
@Test

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,7 @@ public void When_MappingObservationJson_Expect_ObservationStatementXmlOutput(Str
226226
Observation parsedObservation = new FhirParseService().parseResource(jsonInput, Observation.class);
227227

228228
String outputMessage = observationStatementMapper.mapObservationToObservationStatement(parsedObservation, false);
229-
assertThat(outputMessage).isEqualTo(expectedOutputMessage);
229+
assertThat(outputMessage).isEqualToIgnoringWhitespace(expectedOutputMessage);
230230
}
231231

232232
@ParameterizedTest

0 commit comments

Comments
 (0)