Skip to content

Commit b706380

Browse files
Add Schema validation to transformJsonToXml developer tool (#1216)
* Add Schema validation to transform tool * Add ErrorHandler for XML/XSD schema validation errors. * Add Schema validator which retrieves the configured redactions context and validates against the corresponding schema. * Add functionality to output the validation errors in a log file. * Inject and use schema validation when running the transformation process. * Update developer information. * * Add sonar exclusions for transform tool * * Add sonar exclusions for transform tool * Update XmlSchemaValidator to use newDefaultInstance instead * * Add sonar exclusions for transform tool * Update XmlSchemaValidator to use newDefaultInstance instead * * Add sonar exclusions for transform tool * Update XmlSchemaValidator to use newDefaultInstance instead * * Refactor package name to match naming conventions * * Extracted constants for schema validation message templates. * Changed logging to `Error` when the schema file could not be loaded. * Renamed parameter `filename` to be more specific about its intent. * * Rename package folder from `transformJsonToXmlTool` to `transformjsontoxmltool` to address checkstyle package name issues
1 parent c2c3cce commit b706380

File tree

5 files changed

+148
-3
lines changed

5 files changed

+148
-3
lines changed

nhs-england-developer-information.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ We provide a mock MHS adaptor for local development and testing.
191191

192192
### How to transform arbitrary json ASR payload files
193193

194-
This is an interoperability testing tool to transform arbitrary/ad-hoc json ASR payloads and access the outputs.
194+
This is an interoperability testing tool to transform arbitrary/ad-hoc json ASR payloads, access the outputs and validate the produced XML against the relevant schema.
195195

196196
1. Navigate to the input folder and place all Json files to convert here.
197197
`integration-adaptor-gp2gp/transformJsonToXml/input/`
@@ -203,9 +203,11 @@ This is an interoperability testing tool to transform arbitrary/ad-hoc json ASR
203203
./TransformJsonToXml.sh
204204
```
205205

206-
3. The Converted .Xml files will be located in the output folder.
206+
3. The converted XML files will be located in the output folder.
207207
`integration-adaptor-gp2gp/transformJsonToXml/output/`
208208

209+
4. Any schema validation errors will be located with the extension `.validation-errors.log` and will be located in the same output folder as the converted XML files.
210+
209211
## Troubleshooting
210212

211213
### "Invalid source release 17" error

service/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ sonar {
172172
property("sonar.host.url", "https://sonarcloud.io")
173173
property("sonar.projectKey", "NHSDigital_integration-adaptor-gp2gp")
174174
property("sonar.organization", "nhsdigital")
175+
property("sonar.exclusions", "**/transformjsontoxmltool/*")
175176
}
176177
}
177178

service/src/main/java/uk/nhs/adaptors/gp2gp/transformJsonToXmlTool/TransformJsonToXml.java renamed to service/src/main/java/uk/nhs/adaptors/gp2gp/transformjsontoxmltool/TransformJsonToXml.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@
4848
matchIfMissing = false)
4949
@Component
5050
public class TransformJsonToXml implements CommandLineRunner {
51-
5251
private static final String JSON_FILE_INPUT_PATH =
5352
Paths.get("src/").toFile().getAbsoluteFile().getAbsolutePath() + "/../../transformJsonToXml/input/";
5453
private static final String XML_OUTPUT_PATH =
@@ -57,6 +56,7 @@ public class TransformJsonToXml implements CommandLineRunner {
5756
private final MessageContext messageContext;
5857
private final OutputMessageWrapperMapper outputMessageWrapperMapper;
5958
private final EhrExtractMapper ehrExtractMapper;
59+
private final XmlSchemaValidator xmlSchemaValidator;
6060

6161
public static void main(String[] args) {
6262
SpringApplication.run(TransformJsonToXml.class, args).close();
@@ -68,6 +68,7 @@ public void run(String... args) {
6868
getFiles().forEach(file -> {
6969
String xmlResult = mapJsonToXml(file.getJsonFileInput());
7070
writeToFile(xmlResult, file.getJsonFileName());
71+
xmlSchemaValidator.validateOutputToXmlSchema(file.getJsonFileName(), xmlResult);
7172
});
7273
} catch (NHSNumberNotFound | UnreadableJsonFileException | NoJsonFileFound | Hl7TranslatedResponseError e) {
7374
LOGGER.error("error: " + e.getMessage());
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
package uk.nhs.adaptors.gp2gp.transformjsontoxmltool;
2+
3+
import lombok.RequiredArgsConstructor;
4+
import lombok.extern.slf4j.Slf4j;
5+
import org.apache.commons.io.FilenameUtils;
6+
import org.springframework.stereotype.Component;
7+
import org.xml.sax.SAXException;
8+
import org.xml.sax.SAXParseException;
9+
import uk.nhs.adaptors.gp2gp.common.configuration.RedactionsContext;
10+
11+
import javax.xml.transform.stream.StreamSource;
12+
import javax.xml.validation.SchemaFactory;
13+
import javax.xml.validation.Validator;
14+
import java.io.BufferedWriter;
15+
import java.io.File;
16+
import java.io.FileWriter;
17+
import java.io.IOException;
18+
import java.io.StringReader;
19+
import java.nio.charset.StandardCharsets;
20+
import java.nio.file.Paths;
21+
22+
@Component
23+
@Slf4j
24+
@RequiredArgsConstructor()
25+
public class XmlSchemaValidator {
26+
private final RedactionsContext redactionsContext;
27+
28+
private static final String SCHEMA_PATH = "../service/src/test/resources/mim/Schemas/";
29+
private static final String RCMR_IN030000UK06_SCHEMA_PATH = SCHEMA_PATH + RedactionsContext.NON_REDACTION_INTERACTION_ID + ".xsd";
30+
private static final String RCMR_IN030000UK07_SCHEMA_PATH = SCHEMA_PATH + RedactionsContext.REDACTION_INTERACTION_ID + ".xsd";
31+
private static final String OUTPUT_PATH =
32+
Paths.get("src/").toFile().getAbsoluteFile().getAbsolutePath() + "/../../transformJsonToXml/output/";
33+
34+
private static final String VALIDATING_AGAINST_SCHEMA_TEMPLATE = "Validating {} against {} schema";
35+
private static final String COULD_NOT_LOAD_SCHEMA_FILE_TEMPLATE = "Could not load schema file for {} context.";
36+
private static final String FAILED_TO_VALIDATE_SCHEMA_TEMPLATE = "Failed to validate {} against {} schema";
37+
private static final String COULD_NOT_READ_FROM_STREAM_SOURCE_TEMPLATE = "Could not read from stream source for produced XML for {}";
38+
private static final String SUCCESSFULLY_VALIDATED_SCHEMA_TEMPLATE = "Successfully validated {} against {} schema";
39+
private static final String VALIDATION_ERRORS_WRITTEN_TEMPLATE = "Validation errors written to {}";
40+
private static final String COULD_NOT_WRITE_VALIDATION_ERRORS_TEMPLATE = "Could not write validation errors to {}";
41+
42+
public void validateOutputToXmlSchema(String inputJsonFilename, String xmlResult) {
43+
LOGGER.info(VALIDATING_AGAINST_SCHEMA_TEMPLATE, inputJsonFilename, RedactionsContext.REDACTION_INTERACTION_ID);
44+
45+
var xsdErrorHandler = new XsdErrorHandler();
46+
Validator xmlValidator;
47+
48+
try {
49+
xmlValidator = getXmlValidator(xsdErrorHandler);
50+
} catch (SAXException e) {
51+
LOGGER.error(COULD_NOT_LOAD_SCHEMA_FILE_TEMPLATE, RedactionsContext.REDACTION_INTERACTION_ID);
52+
return;
53+
}
54+
55+
try {
56+
var xmlResultSource = new StreamSource(new StringReader(xmlResult));
57+
xmlValidator.validate(xmlResultSource);
58+
} catch (SAXParseException parseException) {
59+
LOGGER.warn(FAILED_TO_VALIDATE_SCHEMA_TEMPLATE, inputJsonFilename, RedactionsContext.REDACTION_INTERACTION_ID);
60+
writeValidationExceptionsToFile(xsdErrorHandler, inputJsonFilename);
61+
} catch (IOException e) {
62+
LOGGER.error(COULD_NOT_READ_FROM_STREAM_SOURCE_TEMPLATE, inputJsonFilename);
63+
return;
64+
} catch (Exception e) {
65+
throw new RuntimeException(e);
66+
}
67+
68+
if (!xsdErrorHandler.isValid()) {
69+
LOGGER.warn(FAILED_TO_VALIDATE_SCHEMA_TEMPLATE, inputJsonFilename, RedactionsContext.REDACTION_INTERACTION_ID);
70+
writeValidationExceptionsToFile(xsdErrorHandler, inputJsonFilename);
71+
return;
72+
}
73+
74+
LOGGER.info(SUCCESSFULLY_VALIDATED_SCHEMA_TEMPLATE, inputJsonFilename, RedactionsContext.REDACTION_INTERACTION_ID);
75+
}
76+
77+
private void writeValidationExceptionsToFile(XsdErrorHandler xsdErrorHandler, String fileName) {
78+
String outputFileName = FilenameUtils.removeExtension(fileName) + ".validation-errors.log";
79+
try (BufferedWriter writer = new BufferedWriter(new FileWriter(OUTPUT_PATH + outputFileName, StandardCharsets.UTF_8))) {
80+
for (SAXParseException e : xsdErrorHandler.getExceptions()) {
81+
var message = String.format("[%d:%d] %s", e.getLineNumber(), e.getColumnNumber(), e.getMessage());
82+
writer.write(message);
83+
writer.newLine();
84+
}
85+
LOGGER.info(VALIDATION_ERRORS_WRITTEN_TEMPLATE, outputFileName);
86+
} catch (IOException e) {
87+
LOGGER.error(COULD_NOT_WRITE_VALIDATION_ERRORS_TEMPLATE, outputFileName, e);
88+
}
89+
}
90+
91+
private Validator getXmlValidator(XsdErrorHandler xsdErrorHandler) throws SAXException {
92+
var schemaPath = RedactionsContext.REDACTION_INTERACTION_ID.equals(redactionsContext.ehrExtractInteractionId())
93+
? RCMR_IN030000UK07_SCHEMA_PATH
94+
: RCMR_IN030000UK06_SCHEMA_PATH;
95+
96+
var schemaFactory = SchemaFactory.newDefaultInstance();
97+
var schemaFileStream = new StreamSource(new File(schemaPath));
98+
var schema = schemaFactory.newSchema(schemaFileStream);
99+
var validator = schema.newValidator();
100+
101+
validator.setErrorHandler(xsdErrorHandler);
102+
103+
return validator;
104+
}
105+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package uk.nhs.adaptors.gp2gp.transformjsontoxmltool;
2+
3+
import lombok.Getter;
4+
import lombok.extern.slf4j.Slf4j;
5+
import org.xml.sax.ErrorHandler;
6+
import org.xml.sax.SAXParseException;
7+
8+
import java.util.ArrayList;
9+
import java.util.List;
10+
11+
@Slf4j
12+
@Getter
13+
public class XsdErrorHandler implements ErrorHandler {
14+
15+
private final List<SAXParseException> exceptions = new ArrayList<>();
16+
17+
public boolean isValid() {
18+
return exceptions.isEmpty();
19+
}
20+
21+
@Override
22+
public void warning(SAXParseException exception) {
23+
exceptions.add(exception);
24+
}
25+
26+
@Override
27+
public void error(SAXParseException exception) {
28+
exceptions.add(exception);
29+
}
30+
31+
@Override
32+
public void fatalError(SAXParseException exception) throws SAXParseException {
33+
exceptions.add(exception);
34+
throw exception;
35+
}
36+
}

0 commit comments

Comments
 (0)