Skip to content

Commit b1e6e27

Browse files
NIAD-2394 - Refactor to remove dependency on the JSON bundle when building a skeleton EHR Extract (#628)
This will allow us to move the compression later on in the GP2GP transfer
1 parent d9e6493 commit b1e6e27

8 files changed

Lines changed: 894 additions & 84 deletions

File tree

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

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import org.springframework.beans.factory.annotation.Autowired;
1111
import org.springframework.beans.factory.annotation.Value;
1212
import org.springframework.stereotype.Component;
13+
1314
import uk.nhs.adaptors.gp2gp.common.service.RandomIdGeneratorService;
1415
import uk.nhs.adaptors.gp2gp.common.service.TimestampService;
1516
import uk.nhs.adaptors.gp2gp.ehr.mapper.parameters.EhrExtractTemplateParameters;
@@ -66,25 +67,10 @@ public EhrExtractTemplateParameters mapBundleToEhrFhirExtractParams(
6667
return ehrExtractTemplateParameters;
6768
}
6869

69-
public String buildSkeletonEhrExtract(GetGpcStructuredTaskDefinition getGpcStructuredTaskDefinition, Bundle bundle, String documentId) {
70-
var ehrCompositionWithNarrativeStatement = buildEhrCompositionForSkeletonEhrExtract(documentId);
71-
EhrExtractTemplateParameters ehrExtractTemplateParameters = setSharedExtractParams(getGpcStructuredTaskDefinition);
72-
73-
ehrExtractTemplateParameters.setAgentDirectory(
74-
agentDirectoryMapper.mapEHRFolderToAgentDirectory(bundle, getPatientNhsNumber(getGpcStructuredTaskDefinition))
75-
);
76-
ehrExtractTemplateParameters.setComponents(List.of(ehrCompositionWithNarrativeStatement));
77-
ehrExtractTemplateParameters.setEffectiveTime(
78-
StatementTimeMappingUtils.prepareEffectiveTimeForEhrFolder(messageContext.getEffectiveTime())
79-
);
80-
return mapEhrExtractToXml(ehrExtractTemplateParameters);
81-
}
82-
8370
public String buildEhrCompositionForSkeletonEhrExtract(String documentId) {
8471
var skeletonComponentTemplateParameters = SkeletonComponentTemplateParameters.builder()
8572
.narrativeStatementId(documentId)
8673
.availabilityTime(DateFormatUtil.toHl7Format(timestampService.now()))
87-
.effectiveTime(StatementTimeMappingUtils.prepareEffectiveTimeForEhrFolder(messageContext.getEffectiveTime()))
8874
.build();
8975
return TemplateUtils.fillTemplate(SKELETON_COMPONENT_TEMPLATE, skeletonComponentTemplateParameters);
9076
}

service/src/main/java/uk/nhs/adaptors/gp2gp/gpc/GetGpcStructuredTaskExecutor.java

Lines changed: 24 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -82,11 +82,15 @@ public void execute(GetGpcStructuredTaskDefinition structuredTaskDefinition) {
8282
try {
8383
messageContext.initialize(structuredRecord);
8484

85-
ehrExtractXml = structuredRecordMappingService.mapStructuredRecordToEhrExtractXml(structuredTaskDefinition, structuredRecord);
85+
ehrExtractXml = structuredRecordMappingService
86+
.mapStructuredRecordToEhrExtractXml(structuredTaskDefinition, structuredRecord);
8687

88+
// Part 2.
8789
LOGGER.info("Checking EHR Extract size");
8890
if (isLargeEhrExtract(ehrExtractXml)) {
8991
LOGGER.info("EHR extract IS large");
92+
var realEhrExtract = ehrExtractXml;
93+
9094
ehrExtractXml = Base64Utils.toBase64String(compress(ehrExtractXml));
9195

9296
var messageId = randomIdGeneratorService.createNewId();
@@ -95,37 +99,38 @@ public void execute(GetGpcStructuredTaskDefinition structuredTaskDefinition) {
9599
var compressedAndEncodedEhrExtractSize = ehrExtractXml.length();
96100

97101
var largeEhrExtractXmlAsExternalAttachment = buildExternalAttachmentForLargeEhrExtract(
98-
compressedAndEncodedEhrExtractSize, messageId, documentId, fileName
102+
compressedAndEncodedEhrExtractSize, messageId, documentId, fileName
99103
);
100104

101105
externalAttachments.add(largeEhrExtractXmlAsExternalAttachment);
102106

103107
var getDocumentTaskDefinition = buildGetDocumentTask(structuredTaskDefinition, largeEhrExtractXmlAsExternalAttachment);
104108
var mhsPayload = ehrDocumentMapper.mapMhsPayloadTemplateToXml(
105-
ehrDocumentMapper.mapToMhsPayloadTemplateParameters(getDocumentTaskDefinition, XML_CONTENT_TYPE));
109+
ehrDocumentMapper.mapToMhsPayloadTemplateParameters(getDocumentTaskDefinition, XML_CONTENT_TYPE));
106110

107111
uploadToStorage(ehrExtractXml, mhsPayload, fileName, getDocumentTaskDefinition);
108112
ehrStatusGpcDocuments.add(EhrExtractStatus.GpcDocument.builder()
109-
.documentId(documentId)
110-
.accessDocumentUrl(null)
111-
.contentType(TEXT_XML_CONTENT_TYPE)
112-
.objectName(fileName)
113-
.fileName(fileName)
114-
.accessedAt(now)
115-
.taskId(getDocumentTaskDefinition.getTaskId())
116-
.messageId(messageId)
117-
.isSkeleton(true)
118-
.identifier(null)
119-
.build());
120-
121-
ehrExtractXml = structuredRecordMappingService
122-
.buildSkeletonEhrExtractXml(structuredTaskDefinition, structuredRecord, documentId);
113+
.documentId(documentId)
114+
.accessDocumentUrl(null)
115+
.contentType(TEXT_XML_CONTENT_TYPE)
116+
.objectName(fileName)
117+
.fileName(fileName)
118+
.accessedAt(now)
119+
.taskId(getDocumentTaskDefinition.getTaskId())
120+
.messageId(messageId)
121+
.isSkeleton(true)
122+
.identifier(null)
123+
.build());
124+
125+
ehrExtractXml = structuredRecordMappingService.buildSkeletonEhrExtractXml(realEhrExtract, documentId);
123126
}
124127

125-
var documentsAsExternalAttachments = structuredRecordMappingService.getExternalAttachments(structuredRecord);
128+
var documentsAsExternalAttachments = structuredRecordMappingService
129+
.getExternalAttachments(structuredRecord);
126130
documentsAsExternalAttachments.stream()
127131
.filter(documentMetadata -> StringUtils.isBlank(documentMetadata.getUrl()))
128-
.peek(absentAttachment -> LOGGER.warn("DocumentReference missing URL: {}", absentAttachment.getDocumentId()))
132+
.peek(absentAttachment ->
133+
LOGGER.warn("DocumentReference missing URL: {}", absentAttachment.getDocumentId()))
129134
.forEach(absentAttachments::add);
130135

131136
documentsAsExternalAttachments = documentsAsExternalAttachments.stream()
@@ -299,7 +304,6 @@ private OutboundMessage.ExternalAttachment buildExternalAttachmentForLargeEhrExt
299304
private void uploadToStorage(String ehrExtract, String mhsPayload, String fileName, DocumentTaskDefinition taskDefinition) {
300305
var taskId = taskDefinition.getTaskId();
301306

302-
// Building outbound message for upload to storage as that is how DocumentSender downloads and parses from storage
303307
var attachments = Collections.singletonList(
304308
OutboundMessage.Attachment.builder()
305309
.contentType(TEXT_XML_CONTENT_TYPE)
Lines changed: 111 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package uk.nhs.adaptors.gp2gp.gpc;
22

33
import lombok.RequiredArgsConstructor;
4+
import lombok.SneakyThrows;
45
import lombok.extern.slf4j.Slf4j;
56

67
import org.apache.commons.lang3.StringUtils;
@@ -11,6 +12,11 @@
1112
import org.springframework.beans.factory.annotation.Autowired;
1213
import org.springframework.stereotype.Service;
1314

15+
import org.w3c.dom.Document;
16+
import org.w3c.dom.Node;
17+
import org.w3c.dom.NodeList;
18+
import org.xml.sax.InputSource;
19+
import org.xml.sax.SAXException;
1420
import uk.nhs.adaptors.gp2gp.common.configuration.Gp2gpConfiguration;
1521
import uk.nhs.adaptors.gp2gp.common.service.RandomIdGeneratorService;
1622
import uk.nhs.adaptors.gp2gp.ehr.EhrExtractStatusService;
@@ -23,6 +29,19 @@
2329
import uk.nhs.adaptors.gp2gp.mhs.model.Identifier;
2430
import uk.nhs.adaptors.gp2gp.mhs.model.OutboundMessage;
2531

32+
import javax.xml.parsers.DocumentBuilder;
33+
import javax.xml.parsers.DocumentBuilderFactory;
34+
import javax.xml.transform.Transformer;
35+
import javax.xml.transform.TransformerException;
36+
import javax.xml.transform.TransformerFactory;
37+
import javax.xml.transform.dom.DOMSource;
38+
import javax.xml.transform.stream.StreamResult;
39+
import javax.xml.xpath.XPathConstants;
40+
import javax.xml.xpath.XPathExpressionException;
41+
import javax.xml.xpath.XPathFactory;
42+
import java.io.IOException;
43+
import java.io.StringReader;
44+
import java.io.StringWriter;
2645
import java.util.List;
2746
import java.util.Optional;
2847
import java.util.stream.Collectors;
@@ -39,26 +58,28 @@ public class StructuredRecordMappingService {
3958
private final SupportedContentTypes supportedContentTypes;
4059
private final EhrExtractStatusService ehrExtractStatusService;
4160

61+
private DocumentBuilder documentBuilder;
62+
4263
public static final String DEFAULT_ATTACHMENT_CONTENT_TYPE = "text/plain";
4364

4465
public List<OutboundMessage.ExternalAttachment> getExternalAttachments(Bundle bundle) {
4566
return ResourceExtractor.extractResourcesByType(bundle, DocumentReference.class)
46-
.filter(documentReference -> !qualifiesAsAbsentAttachment(documentReference))
47-
.map(this::buildExternalAttachment)
48-
.collect(Collectors.toList());
67+
.filter(documentReference -> !qualifiesAsAbsentAttachment(documentReference))
68+
.map(this::buildExternalAttachment)
69+
.collect(Collectors.toList());
4970
}
5071

5172
public List<OutboundMessage.ExternalAttachment> getAbsentAttachments(Bundle bundle) {
5273
return ResourceExtractor.extractResourcesByType(bundle, DocumentReference.class)
53-
.filter(this::qualifiesAsAbsentAttachment)
54-
.map(this::buildExternalAttachment)
55-
.collect(Collectors.toList());
74+
.filter(this::qualifiesAsAbsentAttachment)
75+
.map(this::buildExternalAttachment)
76+
.collect(Collectors.toList());
5677
}
5778

5879
private OutboundMessage.ExternalAttachment buildExternalAttachment(DocumentReference documentReference) {
5980
var attachment = DocumentReferenceUtils.extractAttachment(documentReference);
6081
var documentId = messageContext.getIdMapper()
61-
.newId(ResourceType.DocumentReference, documentReference.getIdElement());
82+
.newId(ResourceType.DocumentReference, documentReference.getIdElement());
6283
var messageId = randomIdGeneratorService.createNewId();
6384

6485
String contentType = DocumentReferenceUtils.extractContentType(attachment);
@@ -71,30 +92,30 @@ private OutboundMessage.ExternalAttachment buildExternalAttachment(DocumentRefer
7192
}
7293

7394
return OutboundMessage.ExternalAttachment.builder()
74-
.documentId(documentId)
75-
.messageId(messageId)
76-
.description(OutboundMessage.AttachmentDescription.builder()
77-
.fileName(fileName)
78-
.contentType(contentType)
79-
.compressed(false) // always false for GPC documents
80-
.largeAttachment(isLargeAttachment(attachment))
81-
.originalBase64(false)
8295
.documentId(documentId)
83-
.build()
84-
.toString()
85-
)
86-
.url(extractUrl(documentReference).orElse(null))
87-
.title(documentReference.getContentFirstRep().getAttachment().getTitle())
88-
.identifier(documentReference.getIdentifier().stream()
89-
.map(identifier -> Identifier.builder()
90-
.system(identifier.getSystem())
91-
.value(identifier.getValue())
92-
.build())
93-
.collect(Collectors.toList()))
94-
.filename(fileName)
95-
.originalDescription(documentReference.getDescription())
96-
.contentType(contentType)
97-
.build();
96+
.messageId(messageId)
97+
.description(OutboundMessage.AttachmentDescription.builder()
98+
.fileName(fileName)
99+
.contentType(contentType)
100+
.compressed(false) // always false for GPC documents
101+
.largeAttachment(isLargeAttachment(attachment))
102+
.originalBase64(false)
103+
.documentId(documentId)
104+
.build()
105+
.toString()
106+
)
107+
.url(extractUrl(documentReference).orElse(null))
108+
.title(documentReference.getContentFirstRep().getAttachment().getTitle())
109+
.identifier(documentReference.getIdentifier().stream()
110+
.map(identifier -> Identifier.builder()
111+
.system(identifier.getSystem())
112+
.value(identifier.getValue())
113+
.build())
114+
.collect(Collectors.toList()))
115+
.filename(fileName)
116+
.originalDescription(documentReference.getDescription())
117+
.contentType(contentType)
118+
.build();
98119
}
99120

100121
private boolean qualifiesAsAbsentAttachment(DocumentReference documentReference) {
@@ -106,19 +127,19 @@ private boolean qualifiesAsAbsentAttachment(DocumentReference documentReference)
106127

107128
private static Optional<String> extractUrl(DocumentReference documentReference) {
108129
return documentReference.getContent().stream()
109-
.map(DocumentReference.DocumentReferenceContentComponent::getAttachment)
110-
.map(Attachment::getUrl)
111-
.peek(url -> {
112-
if (StringUtils.isBlank(url)) {
113-
LOGGER.warn("Empty URL on DocumentReference {}", documentReference.getIdElement().getIdPart());
114-
}
115-
})
116-
.filter(StringUtils::isNotBlank)
117-
.reduce((a, b) -> {
118-
throw new IllegalStateException(String.format(
119-
"There is more than 1 Attachment on DocumentReference %s",
120-
documentReference.getIdElement().getIdPart()));
121-
});
130+
.map(DocumentReference.DocumentReferenceContentComponent::getAttachment)
131+
.map(Attachment::getUrl)
132+
.peek(url -> {
133+
if (StringUtils.isBlank(url)) {
134+
LOGGER.warn("Empty URL on DocumentReference {}", documentReference.getIdElement().getIdPart());
135+
}
136+
})
137+
.filter(StringUtils::isNotBlank)
138+
.reduce((a, b) -> {
139+
throw new IllegalStateException(String.format(
140+
"There is more than 1 Attachment on DocumentReference %s",
141+
documentReference.getIdElement().getIdPart()));
142+
});
122143
}
123144

124145
private boolean isLargeAttachment(Attachment attachment) {
@@ -127,20 +148,61 @@ private boolean isLargeAttachment(Attachment attachment) {
127148

128149
public String mapStructuredRecordToEhrExtractXml(GetGpcStructuredTaskDefinition structuredTaskDefinition, Bundle bundle) {
129150
var ehrExtractTemplateParameters = ehrExtractMapper
130-
.mapBundleToEhrFhirExtractParams(structuredTaskDefinition, bundle);
151+
.mapBundleToEhrFhirExtractParams(structuredTaskDefinition, bundle);
131152
String ehrExtractContent = ehrExtractMapper.mapEhrExtractToXml(ehrExtractTemplateParameters);
132153

133154
ehrExtractStatusService.saveEhrExtractMessageId(structuredTaskDefinition.getConversationId(),
134-
ehrExtractTemplateParameters.getEhrExtractId());
155+
ehrExtractTemplateParameters.getEhrExtractId());
135156

136157
return outputMessageWrapperMapper.map(structuredTaskDefinition, ehrExtractContent);
137158
}
138159

139-
public String buildSkeletonEhrExtractXml(
140-
GetGpcStructuredTaskDefinition structuredTaskDefinition, Bundle structuredRecord, String documentId
160+
@SneakyThrows
161+
public String buildSkeletonEhrExtractXml(String realEhrExtract, String documentId
141162
) {
142-
String ehrExtractContent = ehrExtractMapper.buildSkeletonEhrExtract(structuredTaskDefinition, structuredRecord, documentId);
163+
var ehrCompositionWithNarrativeStatement = ehrExtractMapper.buildEhrCompositionForSkeletonEhrExtract(documentId);
143164

144-
return outputMessageWrapperMapper.map(structuredTaskDefinition, ehrExtractContent);
165+
var documentBuilderFactory = DocumentBuilderFactory.newInstance();
166+
documentBuilder = documentBuilderFactory.newDocumentBuilder();
167+
168+
var document = documentBuilder.parse(new InputSource(new StringReader(realEhrExtract)));
169+
var ehrFolder = removeComponentsFromEhrFolder(document);
170+
insertSkeletonEhrCompositionIntoEhrFolder(ehrCompositionWithNarrativeStatement, ehrFolder);
171+
172+
return toString(document);
173+
}
174+
175+
private static String toString(Document document) throws TransformerException {
176+
TransformerFactory tf = TransformerFactory.newInstance();
177+
Transformer transformer = tf.newTransformer();
178+
StringWriter writer = new StringWriter();
179+
180+
transformer.transform(new DOMSource(document), new StreamResult(writer));
181+
return writer.getBuffer().toString();
182+
}
183+
184+
private void insertSkeletonEhrCompositionIntoEhrFolder(String ehrCompositionWithNarrativeStatement, Node ehrFolder)
185+
throws SAXException, IOException {
186+
var skeletonNode = documentBuilder.parse(new InputSource(new StringReader(ehrCompositionWithNarrativeStatement)));
187+
var skeletonNodeDocumentElement = skeletonNode.getDocumentElement();
188+
189+
ehrFolder.getOwnerDocument().adoptNode(skeletonNodeDocumentElement);
190+
ehrFolder.appendChild(skeletonNodeDocumentElement);
191+
}
192+
193+
private static Node removeComponentsFromEhrFolder(Document document) throws XPathExpressionException {
194+
var xpathFactory = XPathFactory.newInstance();
195+
var xpath = xpathFactory.newXPath();
196+
var xPathExpression = "//EhrExtract//component//ehrFolder//component";
197+
var parentXPathExpression = "//EhrExtract//component//ehrFolder";
198+
199+
var componentNodes = (NodeList) xpath.compile(xPathExpression).evaluate(document, XPathConstants.NODESET);
200+
var parent = (Node) xpath.compile(parentXPathExpression).evaluate(document, XPathConstants.NODE);
201+
202+
for (var i = 0; i < componentNodes.getLength(); i++) {
203+
componentNodes.item(i).getParentNode().removeChild(componentNodes.item(i));
204+
}
205+
206+
return parent;
145207
}
146208
}

0 commit comments

Comments
 (0)