Skip to content
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* The GP2GP Adaptor now populates the ObservationStatement / confidentialityCode field when the .meta.security field of an Uncategorized Data Observation contains NOPAT
* When List.meta.security field contains NOPAT, the GP2GP Adaptor will now populate the CompoundStatement.confidentialityCode
* The GP2GP Adaptor now throws an exception when the Access Structure Record is empty, thereby rejecting the transfer
* The GP2GP Adaptor now throws an exception when the XML is not valid, thereby stopping the transfer from going forward

### Fixed
* When DiagnosticReport doesn't contain a Specimen or Observation reference, instead of "DUMMY" "NOT-PRESENT" value is used
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package uk.nhs.adaptors.gp2gp.ehr.exception;

public class XmlSchemaValidationException extends RuntimeException {

public XmlSchemaValidationException(String message, Throwable cause) {
super(message, cause);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import uk.nhs.adaptors.gp2gp.common.configuration.RedactionsContext;
import uk.nhs.adaptors.gp2gp.common.service.RandomIdGeneratorService;
import uk.nhs.adaptors.gp2gp.common.service.TimestampService;
import uk.nhs.adaptors.gp2gp.ehr.exception.XmlSchemaValidationException;
import uk.nhs.adaptors.gp2gp.ehr.mapper.parameters.EhrExtractTemplateParameters;
import uk.nhs.adaptors.gp2gp.ehr.mapper.parameters.SkeletonComponentTemplateParameters;
import uk.nhs.adaptors.gp2gp.ehr.utils.DateFormatUtil;
Expand All @@ -23,6 +25,15 @@

import uk.nhs.adaptors.gp2gp.ehr.exception.EhrValidationException;

import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import javax.xml.validation.Validator;
import javax.xml.transform.stream.StreamSource;
import org.xml.sax.SAXException;

import java.io.File;
import java.io.IOException;
import java.io.StringReader;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
Expand All @@ -31,9 +42,14 @@
@Component
@Slf4j
public class EhrExtractMapper {
private final RedactionsContext redactionsContext;
private static final Mustache EHR_EXTRACT_TEMPLATE = TemplateUtils.loadTemplate("ehr_extract_template.mustache");
private static final Mustache SKELETON_COMPONENT_TEMPLATE = TemplateUtils.loadTemplate("ehr_skeleton_component_template.mustache");
private static final String CONSULTATION_LIST_CODE = "325851000000107";
private static final String SCHEMA_PATH = "../service/src/test/resources/mim/Schemas/";

private static final String RCMR_IN030000UK06_SCHEMA_PATH = SCHEMA_PATH + RedactionsContext.NON_REDACTION_INTERACTION_ID + ".xsd";
private static final String RCMR_IN030000UK07_SCHEMA_PATH = SCHEMA_PATH + RedactionsContext.REDACTION_INTERACTION_ID + ".xsd";

private final RandomIdGeneratorService randomIdGeneratorService;
private final TimestampService timestampService;
Expand All @@ -49,6 +65,25 @@ public String mapEhrExtractToXml(EhrExtractTemplateParameters ehrExtractTemplate
return TemplateUtils.fillTemplate(EHR_EXTRACT_TEMPLATE, ehrExtractTemplateParameters);
}

public void validateXmlAgainstSchema(String xml) {
String interactionId = redactionsContext.ehrExtractInteractionId();
boolean isRedactionInteraction = RedactionsContext.REDACTION_INTERACTION_ID.equals(interactionId);

String schemaPath = isRedactionInteraction ? RCMR_IN030000UK07_SCHEMA_PATH : RCMR_IN030000UK06_SCHEMA_PATH;
try {
SchemaFactory factory = SchemaFactory.newDefaultInstance();
Schema schema = factory.newSchema(new File(schemaPath));
Validator validator = schema.newValidator();

validator.validate(new StreamSource(new StringReader(xml)));

LOGGER.info("XML successfully validated against schema: {}", schemaPath);
} catch (SAXException | IOException e) {
LOGGER.error("XML validation failed against schema {}: {}", schemaPath, e.getMessage(), e);
throw new XmlSchemaValidationException("XML schema validation failed", e);
}
}

public EhrExtractTemplateParameters mapBundleToEhrFhirExtractParams(
GetGpcStructuredTaskDefinition getGpcStructuredTaskDefinition, Bundle bundle) {
var ehrExtractTemplateParameters = setSharedExtractParams(getGpcStructuredTaskDefinition);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import uk.nhs.adaptors.gp2gp.common.configuration.Gp2gpConfiguration;
import uk.nhs.adaptors.gp2gp.common.service.RandomIdGeneratorService;
import uk.nhs.adaptors.gp2gp.ehr.EhrExtractStatusService;
import uk.nhs.adaptors.gp2gp.ehr.exception.XmlSchemaValidationException;
import uk.nhs.adaptors.gp2gp.ehr.mapper.EhrExtractMapper;
import uk.nhs.adaptors.gp2gp.ehr.mapper.MessageContext;
import uk.nhs.adaptors.gp2gp.ehr.mapper.OutputMessageWrapperMapper;
Expand Down Expand Up @@ -151,6 +152,12 @@ public String mapStructuredRecordToEhrExtractXml(GetGpcStructuredTaskDefinition
.mapBundleToEhrFhirExtractParams(structuredTaskDefinition, bundle);
String ehrExtractContent = ehrExtractMapper.mapEhrExtractToXml(ehrExtractTemplateParameters);

try {
ehrExtractMapper.validateXmlAgainstSchema(ehrExtractContent);
} catch (XmlSchemaValidationException e) {
LOGGER.error("EHR Extract XML validation failed: {}", e.getMessage());
}

ehrExtractStatusService.saveEhrExtractMessageId(structuredTaskDefinition.getConversationId(),
ehrExtractTemplateParameters.getEhrExtractId());

Expand Down Expand Up @@ -206,4 +213,4 @@ private static Node removeComponentsFromEhrFolder(Document document) throws XPat

return parent;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
import uk.nhs.adaptors.gp2gp.common.configuration.RedactionsContext;
import uk.nhs.adaptors.gp2gp.common.service.ConfidentialityService;
import uk.nhs.adaptors.gp2gp.common.service.FhirParseService;
import uk.nhs.adaptors.gp2gp.common.service.RandomIdGeneratorService;
Expand Down Expand Up @@ -101,6 +102,8 @@ class EhrExtractMapperComponentTest {
private CodeableConceptCdMapper codeableConceptCdMapper;
@Mock
private ConfidentialityService confidentialityService;
@Mock
private RedactionsContext redactionsContext;

private NonConsultationResourceMapper nonConsultationResourceMapper;
private EhrExtractMapper ehrExtractMapper;
Expand Down Expand Up @@ -218,7 +221,7 @@ codeableConceptCdMapper, new ParticipantMapper(), confidentialityService),
new BloodPressureValidator()
);

ehrExtractMapper = new EhrExtractMapper(randomIdGeneratorService,
ehrExtractMapper = new EhrExtractMapper(redactionsContext, randomIdGeneratorService,
timestampService,
new EncounterMapper(messageContext, encounterComponentsMapper, confidentialityService),
nonConsultationResourceMapper,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,29 +1,35 @@
package uk.nhs.adaptors.gp2gp.ehr.mapper;

import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.mock;


import java.nio.file.Files;
import java.nio.file.Paths;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Arrays;
import java.util.Collections;

import org.hl7.fhir.dstu3.model.Bundle;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.util.ReflectionTestUtils;

import uk.nhs.adaptors.gp2gp.common.configuration.RedactionsContext;
import uk.nhs.adaptors.gp2gp.common.service.RandomIdGeneratorService;
import uk.nhs.adaptors.gp2gp.common.service.TimestampService;
import uk.nhs.adaptors.gp2gp.ehr.exception.EhrValidationException;
import uk.nhs.adaptors.gp2gp.ehr.exception.XmlSchemaValidationException;
import uk.nhs.adaptors.gp2gp.gpc.GetGpcStructuredTaskDefinition;

@ExtendWith(MockitoExtension.class)
Expand Down Expand Up @@ -52,6 +58,8 @@ class EhrExtractMapperTest {
private EhrFolderEffectiveTime ehrFolderEffectiveTime;
@InjectMocks
private EhrExtractMapper ehrExtractMapper;
@Mock
private RedactionsContext redactionsContext;

@Test
void When_NhsOverrideNumberProvided_Expect_OverrideToBeUsed() {
Expand Down Expand Up @@ -155,4 +163,48 @@ void When_BuildEhrCompositionForSkeletonEhrExtract_Expect_ExpectedComponentBuilt

assertThat(actual).isEqualTo(expected);
}

@Test
void When_ValidateXmlAgainstSchemaWithInvalidXmlAndAnyId_Expect_XmlSchemaValidationExceptionIsThrown() {
String invalidXml = "<invalid><xml>";

when(redactionsContext.ehrExtractInteractionId())
.thenReturn(RedactionsContext.REDACTION_INTERACTION_ID);

XmlSchemaValidationException ex = assertThrows(XmlSchemaValidationException.class, () -> {
ehrExtractMapper.validateXmlAgainstSchema(invalidXml);
});

assertThat(ex.getMessage()).contains("XML schema validation failed");
}

@Test
void When_ValidateXmlAgainstSchemaWithValidXmlAndRedactionId_Expect_NoExceptionIsThrown() throws Exception {
String basePath = Paths.get("src/").toFile().getAbsoluteFile().getAbsolutePath()
+ "/../../service/src/test/resources/";
String xmlFilePath = basePath + "complete-and-validated-xml-test-file-redaction.xml";

String validXml = Files.readString(Paths.get(xmlFilePath));

when(redactionsContext.ehrExtractInteractionId())
.thenReturn(RedactionsContext.REDACTION_INTERACTION_ID);
Assertions.assertTrue(validXml.contains("RCMR_IN030000UK07"));

assertDoesNotThrow(() -> ehrExtractMapper.validateXmlAgainstSchema(validXml));
}
@Test
void When_ValidateXmlAgainstSchemaWithValidXmlAndNonRedactionId_Expect_NoExceptionIsThrown() throws Exception {
String basePath = Paths.get("src/").toFile().getAbsoluteFile().getAbsolutePath()
+ "/../../service/src/test/resources/";
String xmlFilePath = basePath + "complete-and-validated-xml-test-file-non-redaction.xml";

String validXml = Files.readString(Paths.get(xmlFilePath));

Assertions.assertTrue(validXml.contains("RCMR_IN030000UK06"));

when(redactionsContext.ehrExtractInteractionId())
.thenReturn("interaction_id_test");

assertDoesNotThrow(() -> ehrExtractMapper.validateXmlAgainstSchema(validXml));
}
}
Loading