From 0fa7a72ec33e56d1cafabbd79a57858b3197434e Mon Sep 17 00:00:00 2001 From: stuart mcneill Date: Fri, 20 Feb 2026 09:01:24 +0000 Subject: [PATCH 1/3] removed code that replaces 'deliberately witheld' text. --- .../mappings/from/fhir/CompositionMapper.java | 25 ++++----- .../scr/mappings/from/hl7/HtmlParser.java | 56 ++++++++----------- 2 files changed, 35 insertions(+), 46 deletions(-) diff --git a/docker/service/src/main/java/uk/nhs/adaptors/scr/mappings/from/fhir/CompositionMapper.java b/docker/service/src/main/java/uk/nhs/adaptors/scr/mappings/from/fhir/CompositionMapper.java index 4921115a4..537561d04 100644 --- a/docker/service/src/main/java/uk/nhs/adaptors/scr/mappings/from/fhir/CompositionMapper.java +++ b/docker/service/src/main/java/uk/nhs/adaptors/scr/mappings/from/fhir/CompositionMapper.java @@ -20,8 +20,10 @@ import static uk.nhs.adaptors.scr.utils.FhirHelper.randomUUID; /** - * An FHIR SCR will contain multiple resource-types. The parent resourceType will be Bundle. - * Others may include Composition, Practitioner, PractitionerRole, Organization and Patient. + * An FHIR SCR will contain multiple resource-types. The parent resourceType + * will be Bundle. + * Others may include Composition, Practitioner, PractitionerRole, Organization + * and Patient. * This is the FHIR to HL7 mapper of the Composition resource type. * * See: src/test/resources/gp_summary/standard_gp_summary.json @@ -73,21 +75,25 @@ private static void validateCategory(Composition composition) { } Coding category = composition.getCategoryFirstRep().getCodingFirstRep(); if (!SNOMED_SYSTEM.equals(category.getSystem())) { - throw new FhirValidationException("Composition.category.coding.system not supported: " + category.getSystem()); + throw new FhirValidationException( + "Composition.category.coding.system not supported: " + category.getSystem()); } if (!CARE_PROFESSIONAL_DOC_CODE.equals(category.getCode())) { throw new FhirValidationException("Composition.category.coding.code not supported: " + category.getCode()); } if (!CARE_PROFESSIONAL_DOC_DISPLAY.equals(category.getDisplay())) { - throw new FhirValidationException("Composition.category.coding.display not supported: " + category.getDisplay()); + throw new FhirValidationException( + "Composition.category.coding.display not supported: " + category.getDisplay()); } } - private static void setCompositionRelatesToId(GpSummary gpSummary, Composition composition) throws FhirMappingException { + private static void setCompositionRelatesToId(GpSummary gpSummary, Composition composition) + throws FhirMappingException { if (composition.hasRelatesTo()) { var relatesTo = composition.getRelatesToFirstRep(); if (!REPLACES.equals(relatesTo.getCode())) { - throw new FhirValidationException("Unsupported Composition.relatesTo.code element: " + relatesTo.getCode()); + throw new FhirValidationException( + "Unsupported Composition.relatesTo.code element: " + relatesTo.getCode()); } if (relatesTo.getTargetIdentifier().hasValue()) { gpSummary.setCompositionRelatesToId(relatesTo.getTargetIdentifier().getValue()); @@ -138,13 +144,6 @@ private static void setPresentation(GpSummary gpSummary, Composition composition removeEmptyNodes(divDocument); var divChildNodes = divDocument.getDocumentElement().getChildNodes(); for (int i = 0; i < divChildNodes.getLength(); i++) { - String toReplaceWith = divChildNodes.item(i).getTextContent(); - - // There has previously been incorrect verbiage in some SCRs. This helps to standardise language. - if (toReplaceWith.contains("One or more entries have been deliberately withheld from this GP Summary.")) { - toReplaceWith = toReplaceWith.replace("deliberately withheld", "intentionally withheld"); - divChildNodes.item(i).setTextContent(toReplaceWith); - } bodyNode.appendChild(htmlDocument.importNode(divChildNodes.item(i), true)); } } diff --git a/docker/service/src/main/java/uk/nhs/adaptors/scr/mappings/from/hl7/HtmlParser.java b/docker/service/src/main/java/uk/nhs/adaptors/scr/mappings/from/hl7/HtmlParser.java index 2a2790a3b..73542247b 100644 --- a/docker/service/src/main/java/uk/nhs/adaptors/scr/mappings/from/hl7/HtmlParser.java +++ b/docker/service/src/main/java/uk/nhs/adaptors/scr/mappings/from/hl7/HtmlParser.java @@ -48,21 +48,22 @@ public List parse(Node html) { removeEmptyNodes(html); var bodyNode = xmlUtils.getNodesByXPath(html, "./body").stream() - .findFirst() - .orElseThrow(() -> new IllegalStateException("Missing body node in html")); + .findFirst() + .orElseThrow(() -> new IllegalStateException("Missing body node in html")); var childNodes = bodyNode.getChildNodes(); if (childNodes.getLength() != 0 && !H2.equals(childNodes.item(0).getNodeName())) { throw new IllegalStateException("First body node must be H2"); } - // this will hold a list of paris: H2 node as key and all next nodes (until next H2 or body end) as a new Document + // this will hold a list of paris: H2 node as key and all next nodes (until next + // H2 or body end) as a new Document var items = new ArrayList>(); Document targetDocument = null; for (int i = 0; i < childNodes.getLength(); i++) { var currentNode = childNodes.item(i); if (H2.equals(currentNode.getNodeName())) { - //since this is the H2, we begin to capture following nodes as a new Document + // since this is the H2, we begin to capture following nodes as a new Document targetDocument = createNewDocument("div", "http://www.w3.org/1999/xhtml"); items.add(Pair.of(currentNode, targetDocument)); } else { @@ -70,22 +71,22 @@ public List parse(Node html) { throw new IllegalStateException("Target document not initialized"); } targetDocument.getDocumentElement() - .appendChild(targetDocument.importNode(currentNode, true)); + .appendChild(targetDocument.importNode(currentNode, true)); } } return items.stream() - .map(kv -> ParsedHtml - .builder() - .html(serialize(kv.getValue())) - .h2Value(kv.getKey().getTextContent()) - .h2Id(Optional.ofNullable(kv.getKey().getAttributes()) - .map(h2IdAttribute -> h2IdAttribute.getNamedItem("id")) - .map(Node::getNodeValue) - .orElse(null)) - .build()) - .map(HtmlParser::buildSectionComponent) - .collect(Collectors.toList()); + .map(kv -> ParsedHtml + .builder() + .html(serialize(kv.getValue())) + .h2Value(kv.getKey().getTextContent()) + .h2Id(Optional.ofNullable(kv.getKey().getAttributes()) + .map(h2IdAttribute -> h2IdAttribute.getNamedItem("id")) + .map(Node::getNodeValue) + .orElse(null)) + .build()) + .map(HtmlParser::buildSectionComponent) + .collect(Collectors.toList()); } @SneakyThrows @@ -107,41 +108,30 @@ public static String serialize(Document document) { @SneakyThrows public static void removeEmptyNodes(Node document) { XPathExpression xpathExp = XPathFactory.newInstance().newXPath() - .compile("//text()[normalize-space(.) = '']"); + .compile("//text()[normalize-space(.) = '']"); NodeList emptyTextNodes = (NodeList) xpathExp.evaluate(document, XPathConstants.NODESET); for (int i = 0; i < emptyTextNodes.getLength(); i++) { Node emptyTextNode = emptyTextNodes.item(i); emptyTextNode.getParentNode().removeChild(emptyTextNode); } - } - private static Composition.SectionComponent buildSectionComponent(ParsedHtml parsedHtml) { var sectionComponent = new Composition.SectionComponent() - .setTitle(parsedHtml.h2Value) - .setText(buildNarrative(parsedHtml.html)); + .setTitle(parsedHtml.h2Value) + .setText(buildNarrative(parsedHtml.html)); if (StringUtils.isNotBlank(parsedHtml.h2Id)) { sectionComponent.setCode(new CodeableConcept() - .addCoding(new Coding() - .setCode(parsedHtml.h2Id))); + .addCoding(new Coding() + .setCode(parsedHtml.h2Id))); } - String toBeReplaced = sectionComponent.getText().getDiv().getValue(); - - if (toBeReplaced.contains("One or more entries have been deliberately withheld from this GP Summary.")) { - toBeReplaced = toBeReplaced.replace( - "One or more entries have been deliberately withheld from this GP Summary.", - "One or more entries have been withheld from this GP Summary."); - sectionComponent.getText().getDiv().setValue(toBeReplaced); - } - return sectionComponent; } private static Narrative buildNarrative(String html) { var narrative = new Narrative() - .setStatus(Narrative.NarrativeStatus.GENERATED); + .setStatus(Narrative.NarrativeStatus.GENERATED); narrative.setDivAsString(html); return narrative; } From 5072657bef3ad08b4acd511cb930586a41f1cf1a Mon Sep 17 00:00:00 2001 From: stuart mcneill Date: Fri, 20 Feb 2026 13:52:44 +0000 Subject: [PATCH 2/3] restored white space changes. --- .../mappings/from/fhir/CompositionMapper.java | 18 +++---- .../scr/mappings/from/hl7/HtmlParser.java | 47 ++++++++++--------- 2 files changed, 30 insertions(+), 35 deletions(-) diff --git a/docker/service/src/main/java/uk/nhs/adaptors/scr/mappings/from/fhir/CompositionMapper.java b/docker/service/src/main/java/uk/nhs/adaptors/scr/mappings/from/fhir/CompositionMapper.java index 537561d04..cd77c9a89 100644 --- a/docker/service/src/main/java/uk/nhs/adaptors/scr/mappings/from/fhir/CompositionMapper.java +++ b/docker/service/src/main/java/uk/nhs/adaptors/scr/mappings/from/fhir/CompositionMapper.java @@ -20,10 +20,8 @@ import static uk.nhs.adaptors.scr.utils.FhirHelper.randomUUID; /** - * An FHIR SCR will contain multiple resource-types. The parent resourceType - * will be Bundle. - * Others may include Composition, Practitioner, PractitionerRole, Organization - * and Patient. + * An FHIR SCR will contain multiple resource-types. The parent resourceType will be Bundle. + * Others may include Composition, Practitioner, PractitionerRole, Organization and Patient. * This is the FHIR to HL7 mapper of the Composition resource type. * * See: src/test/resources/gp_summary/standard_gp_summary.json @@ -75,25 +73,21 @@ private static void validateCategory(Composition composition) { } Coding category = composition.getCategoryFirstRep().getCodingFirstRep(); if (!SNOMED_SYSTEM.equals(category.getSystem())) { - throw new FhirValidationException( - "Composition.category.coding.system not supported: " + category.getSystem()); + throw new FhirValidationException("Composition.category.coding.system not supported: " + category.getSystem()); } if (!CARE_PROFESSIONAL_DOC_CODE.equals(category.getCode())) { throw new FhirValidationException("Composition.category.coding.code not supported: " + category.getCode()); } if (!CARE_PROFESSIONAL_DOC_DISPLAY.equals(category.getDisplay())) { - throw new FhirValidationException( - "Composition.category.coding.display not supported: " + category.getDisplay()); + throw new FhirValidationException("Composition.category.coding.display not supported: " + category.getDisplay()); } } - private static void setCompositionRelatesToId(GpSummary gpSummary, Composition composition) - throws FhirMappingException { + private static void setCompositionRelatesToId(GpSummary gpSummary, Composition composition) throws FhirMappingException { if (composition.hasRelatesTo()) { var relatesTo = composition.getRelatesToFirstRep(); if (!REPLACES.equals(relatesTo.getCode())) { - throw new FhirValidationException( - "Unsupported Composition.relatesTo.code element: " + relatesTo.getCode()); + throw new FhirValidationException("Unsupported Composition.relatesTo.code element: " + relatesTo.getCode()); } if (relatesTo.getTargetIdentifier().hasValue()) { gpSummary.setCompositionRelatesToId(relatesTo.getTargetIdentifier().getValue()); diff --git a/docker/service/src/main/java/uk/nhs/adaptors/scr/mappings/from/hl7/HtmlParser.java b/docker/service/src/main/java/uk/nhs/adaptors/scr/mappings/from/hl7/HtmlParser.java index 73542247b..d95bcc667 100644 --- a/docker/service/src/main/java/uk/nhs/adaptors/scr/mappings/from/hl7/HtmlParser.java +++ b/docker/service/src/main/java/uk/nhs/adaptors/scr/mappings/from/hl7/HtmlParser.java @@ -48,22 +48,21 @@ public List parse(Node html) { removeEmptyNodes(html); var bodyNode = xmlUtils.getNodesByXPath(html, "./body").stream() - .findFirst() - .orElseThrow(() -> new IllegalStateException("Missing body node in html")); + .findFirst() + .orElseThrow(() -> new IllegalStateException("Missing body node in html")); var childNodes = bodyNode.getChildNodes(); if (childNodes.getLength() != 0 && !H2.equals(childNodes.item(0).getNodeName())) { throw new IllegalStateException("First body node must be H2"); } - // this will hold a list of paris: H2 node as key and all next nodes (until next - // H2 or body end) as a new Document + // this will hold a list of paris: H2 node as key and all next nodes (until next H2 or body end) as a new Document var items = new ArrayList>(); Document targetDocument = null; for (int i = 0; i < childNodes.getLength(); i++) { var currentNode = childNodes.item(i); if (H2.equals(currentNode.getNodeName())) { - // since this is the H2, we begin to capture following nodes as a new Document + //since this is the H2, we begin to capture following nodes as a new Document targetDocument = createNewDocument("div", "http://www.w3.org/1999/xhtml"); items.add(Pair.of(currentNode, targetDocument)); } else { @@ -71,22 +70,22 @@ public List parse(Node html) { throw new IllegalStateException("Target document not initialized"); } targetDocument.getDocumentElement() - .appendChild(targetDocument.importNode(currentNode, true)); + .appendChild(targetDocument.importNode(currentNode, true)); } } return items.stream() - .map(kv -> ParsedHtml - .builder() - .html(serialize(kv.getValue())) - .h2Value(kv.getKey().getTextContent()) - .h2Id(Optional.ofNullable(kv.getKey().getAttributes()) - .map(h2IdAttribute -> h2IdAttribute.getNamedItem("id")) - .map(Node::getNodeValue) - .orElse(null)) - .build()) - .map(HtmlParser::buildSectionComponent) - .collect(Collectors.toList()); + .map(kv -> ParsedHtml + .builder() + .html(serialize(kv.getValue())) + .h2Value(kv.getKey().getTextContent()) + .h2Id(Optional.ofNullable(kv.getKey().getAttributes()) + .map(h2IdAttribute -> h2IdAttribute.getNamedItem("id")) + .map(Node::getNodeValue) + .orElse(null)) + .build()) + .map(HtmlParser::buildSectionComponent) + .collect(Collectors.toList()); } @SneakyThrows @@ -108,30 +107,32 @@ public static String serialize(Document document) { @SneakyThrows public static void removeEmptyNodes(Node document) { XPathExpression xpathExp = XPathFactory.newInstance().newXPath() - .compile("//text()[normalize-space(.) = '']"); + .compile("//text()[normalize-space(.) = '']"); NodeList emptyTextNodes = (NodeList) xpathExp.evaluate(document, XPathConstants.NODESET); for (int i = 0; i < emptyTextNodes.getLength(); i++) { Node emptyTextNode = emptyTextNodes.item(i); emptyTextNode.getParentNode().removeChild(emptyTextNode); } + } + private static Composition.SectionComponent buildSectionComponent(ParsedHtml parsedHtml) { var sectionComponent = new Composition.SectionComponent() - .setTitle(parsedHtml.h2Value) - .setText(buildNarrative(parsedHtml.html)); + .setTitle(parsedHtml.h2Value) + .setText(buildNarrative(parsedHtml.html)); if (StringUtils.isNotBlank(parsedHtml.h2Id)) { sectionComponent.setCode(new CodeableConcept() - .addCoding(new Coding() - .setCode(parsedHtml.h2Id))); + .addCoding(new Coding() + .setCode(parsedHtml.h2Id))); } return sectionComponent; } private static Narrative buildNarrative(String html) { var narrative = new Narrative() - .setStatus(Narrative.NarrativeStatus.GENERATED); + .setStatus(Narrative.NarrativeStatus.GENERATED); narrative.setDivAsString(html); return narrative; } From 74224d5faf8c50c80bb858321c01a6ba404b537d Mon Sep 17 00:00:00 2001 From: stuart mcneill Date: Sun, 22 Feb 2026 17:49:18 +0000 Subject: [PATCH 3/3] FLAGSAPI-1515 updated some tests to expect 'deliberately witheld' --- ...al_information_gp_summary_1_presentation_text_value.html | 4 ++-- .../additional_information_gp_summary_1_composition.json | 4 ++-- .../src/test/resources/html_parser/example_withheld.json | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docker/service/src/test/resources/gp_summary/from/fhir/additional_information_gp_summary_1_presentation_text_value.html b/docker/service/src/test/resources/gp_summary/from/fhir/additional_information_gp_summary_1_presentation_text_value.html index 6896a9bee..915c641ab 100644 --- a/docker/service/src/test/resources/gp_summary/from/fhir/additional_information_gp_summary_1_presentation_text_value.html +++ b/docker/service/src/test/resources/gp_summary/from/fhir/additional_information_gp_summary_1_presentation_text_value.html @@ -3,7 +3,7 @@

General Practice Summary

Sourced from the patient's General Practice record. This summary may not include all the information pertinent to this patient.

Summary Created: 30 Apr 2020 16:13

-

One or more entries have been intentionally withheld from this GP Summary.

+

One or more entries have been deliberately withheld from this GP Summary.

Created by: WILSON, Bob (Mr)

Spine Testing, The Practice, Woodvale Terrace, Horsforth, Leeds LS18 4JW

Allergies and Adverse Reactions

@@ -316,7 +316,7 @@

Treatments

Social and Personal Circumstances

-

One or more entries have been intentionally withheld from this GP Summary.

+

One or more entries have been deliberately withheld from this GP Summary.

diff --git a/docker/service/src/test/resources/gp_summary/partials/additional_information_gp_summary_1_composition.json b/docker/service/src/test/resources/gp_summary/partials/additional_information_gp_summary_1_composition.json index 51d222657..c35a2118e 100644 --- a/docker/service/src/test/resources/gp_summary/partials/additional_information_gp_summary_1_composition.json +++ b/docker/service/src/test/resources/gp_summary/partials/additional_information_gp_summary_1_composition.json @@ -55,7 +55,7 @@ }, "text": { "status": "generated", - "div": "

Sourced from the patient's General Practice record. This summary may not include all the information pertinent to this patient.

Summary Created: 30 Apr 2020 16:13

One or more entries have been withheld from this GP Summary.

Created by: WILSON, Bob (Mr)

Spine Testing, The Practice, Woodvale Terrace, Horsforth, Leeds LS18 4JW

" + "div": "

Sourced from the patient's General Practice record. This summary may not include all the information pertinent to this patient.

Summary Created: 30 Apr 2020 16:13

One or more entries have been deliberately withheld from this GP Summary.

Created by: WILSON, Bob (Mr)

Spine Testing, The Practice, Woodvale Terrace, Horsforth, Leeds LS18 4JW

" } }, { @@ -181,7 +181,7 @@ }, "text": { "status": "generated", - "div": "

One or more entries have been withheld from this GP Summary.

DateDescriptionValueSupporting Information
01 Apr 2020Main spoken language EnglishBilingual
31 Mar 2020Main spoken language English[No notes were originally recorded]
26 Mar 2020Main spoken language Mandarin

Summary Sent: 30 Apr 2020 16:13

" + "div": "

One or more entries have been deliberately withheld from this GP Summary.

DateDescriptionValueSupporting Information
01 Apr 2020Main spoken language EnglishBilingual
31 Mar 2020Main spoken language English[No notes were originally recorded]
26 Mar 2020Main spoken language Mandarin

Summary Sent: 30 Apr 2020 16:13

" } } ] diff --git a/docker/service/src/test/resources/html_parser/example_withheld.json b/docker/service/src/test/resources/html_parser/example_withheld.json index e3fed7bf3..5b28e0965 100644 --- a/docker/service/src/test/resources/html_parser/example_withheld.json +++ b/docker/service/src/test/resources/html_parser/example_withheld.json @@ -9,7 +9,7 @@ }, "text": { "status": "generated", - "div": "

Sourced from the patient's General Practice record. This summary may not include all the information pertinent to this patient.

Summary Created: 30 Apr 2020 16:13

One or more entries have been withheld from this GP Summary.

Created by: WILSON, Bob (Mr)

Spine Testing, The Practice, Woodvale Terrace, Horsforth, Leeds LS18 4JW

" + "div": "

Sourced from the patient's General Practice record. This summary may not include all the information pertinent to this patient.

Summary Created: 30 Apr 2020 16:13

One or more entries have been deliberately withheld from this GP Summary.

Created by: WILSON, Bob (Mr)

Spine Testing, The Practice, Woodvale Terrace, Horsforth, Leeds LS18 4JW

" } }, { "title": "Allergies and Adverse Reactions", @@ -108,7 +108,7 @@ }, "text": { "status": "generated", - "div": "

One or more entries have been withheld from this GP Summary.

DateDescriptionValueSupporting Information
01 Apr 2020Main spoken language EnglishBilingual
31 Mar 2020Main spoken language English[No notes were originally recorded]
26 Mar 2020Main spoken language Mandarin

Summary Sent: 30 Apr 2020 16:13

" + "div": "

One or more entries have been deliberately withheld from this GP Summary.

DateDescriptionValueSupporting Information
01 Apr 2020Main spoken language EnglishBilingual
31 Mar 2020Main spoken language English[No notes were originally recorded]
26 Mar 2020Main spoken language Mandarin

Summary Sent: 30 Apr 2020 16:13

" } } ] -} \ No newline at end of file +}