This commit is contained in:
Grahame Grieve 2024-02-26 11:48:16 +11:00
commit ea899b92a7
8 changed files with 401 additions and 18 deletions

View File

@ -123,6 +123,16 @@ public class SimpleWorkerContextTests {
} }
} }
public class CodingMatcher implements ArgumentMatcher<Coding> {
final private Coding left;
CodingMatcher(Coding left) { this.left = left; }
public boolean matches(Coding right) {
return left.equalsShallow(right);
}
}
public class ParametersMatcher implements ArgumentMatcher<Parameters> { public class ParametersMatcher implements ArgumentMatcher<Parameters> {
final private Parameters left; final private Parameters left;
@ -187,7 +197,7 @@ public class SimpleWorkerContextTests {
assertEquals(expectedValidationResult, actualValidationResult); assertEquals(expectedValidationResult, actualValidationResult);
// Mockito.verify(valueSetCheckerSimple).validateCode("Coding", coding); Mockito.verify(valueSetCheckerSimple).validateCode(eq("Coding"), argThat(new CodingMatcher(coding)));
Mockito.verify(terminologyCache).getValidation(cacheToken); Mockito.verify(terminologyCache).getValidation(cacheToken);
Mockito.verify(terminologyCache).cacheValidation(cacheToken, expectedValidationResult,false); Mockito.verify(terminologyCache).cacheValidation(cacheToken, expectedValidationResult,false);
} }

View File

@ -0,0 +1,199 @@
package org.hl7.fhir.r5.test;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.FileSystems;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Properties;
import org.fhir.ucum.UcumException;
import org.hl7.fhir.r5.conformance.profile.ProfileUtilities;
import org.hl7.fhir.r5.context.IWorkerContext;
import org.hl7.fhir.r5.elementmodel.Element;
import org.hl7.fhir.r5.elementmodel.Manager;
import org.hl7.fhir.r5.elementmodel.Manager.FhirFormat;
import org.hl7.fhir.r5.elementmodel.ResourceParser;
import org.hl7.fhir.r5.elementmodel.TurtleParser;
import org.hl7.fhir.r5.elementmodel.XmlParser;
import org.hl7.fhir.r5.formats.IParser.OutputStyle;
import org.hl7.fhir.r5.model.Resource;
import org.hl7.fhir.r5.test.utils.TestingUtilities;
import org.hl7.fhir.utilities.validation.ValidationMessage;
import org.hl7.fhir.utilities.turtle.Turtle;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
/**
* TurtleGeneratorTests
* Generates turtle files from specified resources, including example "instances"
* Unit tests for the generated turtle files
* For generic RDF parsing tests, see `TurtleTests.java`
* For ShEx validation tests, see `ShExGeneratorTests.java`
* Author: Tim Prudhomme <tmprdh@gmail.com>
*/
public class TurtleGeneratorTests {
private static IWorkerContext workerContext;
private static ResourceParser resourceParser;
private static XmlParser xmlParser;
private static TurtleParser turtleParser;
private static Path inputXmlDirectory;
private static Path outputTurtleDirectory;
@BeforeAll
public static void setup() throws IOException {
workerContext = TestingUtilities.getSharedWorkerContext();
resourceParser = new org.hl7.fhir.r5.elementmodel.ResourceParser(workerContext);
xmlParser = (XmlParser) Manager.makeParser(workerContext, FhirFormat.XML);
turtleParser = (TurtleParser) Manager.makeParser(workerContext, FhirFormat.TURTLE);
// Temporary directory of files that should be discarded after testing
outputTurtleDirectory = FileSystems.getDefault().getPath(System.getProperty("java.io.tmpdir"));
// Directory of XML files used for generating Turtle files
String currentDirectory = System.getProperty("user.dir");
inputXmlDirectory = FileSystems.getDefault().getPath(currentDirectory, "src", "test", "resources", "testUtilities", "xml", "examples");
}
@Test
public void testExamples() throws IOException, UcumException {
var exampleInstanceName = "codesystem-contact-point-use";
testInstanceGeneration(exampleInstanceName);
}
@Disabled("TODO this doesn't pass due to existing issues in R5 RDF")
@Test
public void testProfiles() throws IOException, UcumException {
var profileName = "Encounter";
testClassGeneration(profileName);
}
@Disabled("Run manually for testing with XML resources generated from FHIR specification publishing library")
@Test
public void testPublishedExamples() throws IOException, UcumException {
inputXmlDirectory = getPublishedXmlDirectory();
var exampleInstanceName = "codesystem-contact-point-use";
testInstanceGeneration(exampleInstanceName);
}
/*
* Generate a Turtle file from the name of an XML resource, then parse it
*/
private void testInstanceGeneration(String resourceName) throws IOException, UcumException {
// Generate Turtle
var generatedTurtleFilePath = generateTurtleFromResourceName(resourceName, inputXmlDirectory, outputTurtleDirectory);
// Try parsing again ("round-trip test") -- this only tests for valid RDF
parseGeneratedTurtle(generatedTurtleFilePath);
}
/*
* Generate a Turtle file from the name of a profile, then parse it
*/
private void testClassGeneration(String profileName) throws IOException, UcumException {
var generatedTurtleFilePath = generateTurtleClassFromProfileName(profileName);
// Try parsing again ("round-trip test") -- this only tests for valid RDF
parseGeneratedTurtle(generatedTurtleFilePath);
}
private void parseGeneratedTurtle(String generatedTurtleFilePath) throws IOException {
try (
InputStream turtleStream = new FileInputStream(generatedTurtleFilePath);
) {
var generatedTurtleString = new String(turtleStream.readAllBytes());
Turtle ttl = new Turtle();
ttl.parse(generatedTurtleString);
}
}
/**
* Generate a Turtle version of a resource, given its name, input directory of its XML source, and output directory of the Turtle file
* @return the path of the generated Turtle file
*/
private String generateTurtleFromResourceName(String resourceName, Path inputXmlDirectory, Path outputTurtleDirectory) throws IOException, UcumException {
// Specify source xml path and destination turtle path
var xmlFilePath = inputXmlDirectory.resolve(resourceName + ".xml").toString();
var turtleFilePath = outputTurtleDirectory.resolve(resourceName + ".ttl").toString();
try (
InputStream inputXmlStream = new FileInputStream(xmlFilePath);
OutputStream outputTurtleStream = new FileOutputStream(turtleFilePath);
) {
// print out file names using string interpolation
System.out.println("Generating " + turtleFilePath);
generateTurtleFromXmlStream(inputXmlStream, outputTurtleStream);
return turtleFilePath;
}
}
/**
* Generate a Turtle file from an XML resource
*/
private void generateTurtleFromXmlStream(InputStream xmlStream, OutputStream turtleStream) throws IOException, UcumException {
var errorList = new ArrayList<ValidationMessage>();
Element resourceElement = xmlParser.parseSingle(xmlStream, errorList);
turtleParser.compose(resourceElement, turtleStream, OutputStyle.PRETTY, null);
// Check errors
for (ValidationMessage m : errorList) {
System.out.println(m.getDisplay());
}
}
/**
* Generate a Turtle file from an org.hl7.fhir.r5.model.Resource profile
* @return the path of the generated Turtle file
*/
private String generateTurtleClassFromProfileName(String profileName) throws IOException, UcumException {
String resourceUri = ProfileUtilities.sdNs(profileName, null);
Resource resource = workerContext.fetchResource(Resource.class, resourceUri);
Element resourceElement = resourceParser.parse(resource);
var turtleFilePath = outputTurtleDirectory.resolve(profileName + ".ttl").toString();
try (OutputStream outputStream = new FileOutputStream(turtleFilePath)) {
turtleParser.compose(resourceElement, outputStream, OutputStyle.PRETTY, null);
return turtleFilePath;
}
}
/**
* Generate a Turtle file from a "test case" resource -- those only available on https://github.com/FHIR/fhir-test-cases/
* @return the path of the generated Turtle file
*/
private String generateTurtleFromTestCaseResource(String resourceName) throws IOException, UcumException {
var turtleFilePath = outputTurtleDirectory.resolve(resourceName + ".ttl").toString();
try (
// Follows pattern in `TestingUtilities.java`
InputStream inputXmlStream = TestingUtilities.loadTestResourceStream("r5", resourceName + ".xml");
OutputStream outputTurtleStream = new FileOutputStream(turtleFilePath);
) {
generateTurtleFromXmlStream(inputXmlStream, outputTurtleStream);
return turtleFilePath;
}
}
/**
* This could be the "publish" directory of XML resources built using the FHIR specification publishing library.
* Use this for testing with other generated XML resources
*/
private static Path getPublishedXmlDirectory() throws IOException {
Properties properties = new Properties();
String currentDirectory = System.getProperty("user.dir");
// Add your directory path to "org.hl7.fhir.r5/src/test/resources/local.properties"
String localPropertiesPath = FileSystems.getDefault().getPath(currentDirectory, "src", "test", "resources", "local.properties").toString();
try (FileInputStream input = new FileInputStream(localPropertiesPath)) {
properties.load(input);
} catch (IOException e) {
// You should create this local.properties file if it doesn't exist. It should already be listed in .gitignore.
e.printStackTrace();
throw e;
}
var filePath = properties.getProperty("xmlResourceDirectory");
return FileSystems.getDefault().getPath(filePath);
}
}

View File

@ -0,0 +1,134 @@
<?xml version="1.0" encoding="UTF-8"?>
<CodeSystem xmlns="http://hl7.org/fhir">
<id value="contact-point-use"/>
<meta>
<lastUpdated value="2023-10-03T22:51:29.574-04:00"/>
</meta>
<text>
<status value="generated"/>
<div xmlns="http://www.w3.org/1999/xhtml">
<p>This case-sensitive code system
<code>http://hl7.org/fhir/contact-point-use</code> defines the following codes:
</p>
<table class="codes">
<tr>
<td style="white-space:nowrap">
<b>Code</b>
</td>
<td>
<b>Display</b>
</td>
<td>
<b>Definition</b>
</td>
</tr>
<tr>
<td style="white-space:nowrap">home
<a name="contact-point-use-home"> </a>
</td>
<td>Home</td>
<td>A communication contact point at a home; attempted contacts for business purposes might intrude privacy and chances are one will contact family or other household members instead of the person one wishes to call. Typically used with urgent cases, or if no other contacts are available.</td>
</tr>
<tr>
<td style="white-space:nowrap">work
<a name="contact-point-use-work"> </a>
</td>
<td>Work</td>
<td>An office contact point. First choice for business related contacts during business hours.</td>
</tr>
<tr>
<td style="white-space:nowrap">temp
<a name="contact-point-use-temp"> </a>
</td>
<td>Temp</td>
<td>A temporary contact point. The period can provide more detailed information.</td>
</tr>
<tr>
<td style="white-space:nowrap">old
<a name="contact-point-use-old"> </a>
</td>
<td>Old</td>
<td>This contact point is no longer in use (or was never correct, but retained for records).</td>
</tr>
<tr>
<td style="white-space:nowrap">mobile
<a name="contact-point-use-mobile"> </a>
</td>
<td>Mobile</td>
<td>A telecommunication device that moves and stays with its owner. May have characteristics of all other use codes, suitable for urgent matters, not the first choice for routine business.</td>
</tr>
</table>
</div>
</text>
<extension url="http://hl7.org/fhir/StructureDefinition/structuredefinition-wg">
<valueCode value="fhir"/>
</extension>
<extension url="http://hl7.org/fhir/StructureDefinition/structuredefinition-standards-status">
<valueCode value="normative"/>
</extension>
<extension url="http://hl7.org/fhir/StructureDefinition/structuredefinition-normative-version">
<valueCode value="4.0.0"/>
</extension>
<extension url="http://hl7.org/fhir/StructureDefinition/structuredefinition-fmm">
<valueInteger value="5"/>
</extension>
<url value="http://hl7.org/fhir/contact-point-use"/>
<identifier>
<system value="urn:ietf:rfc:3986"/>
<value value="urn:oid:2.16.840.1.113883.4.642.1.74"/>
</identifier>
<version value="6.0.0-cibuild"/>
<name value="ContactPointUse"/>
<title value="ContactPointUse"/>
<status value="active"/>
<experimental value="false"/>
<date value="2023-10-03T22:51:29-04:00"/>
<publisher value="HL7 (FHIR Project)"/>
<contact>
<telecom>
<system value="url"/>
<value value="http://hl7.org/fhir"/>
</telecom>
<telecom>
<system value="email"/>
<value value="fhir@lists.hl7.org"/>
</telecom>
</contact>
<description value="Use of contact point."/>
<jurisdiction>
<coding>
<system value="http://unstats.un.org/unsd/methods/m49/m49.htm"/>
<code value="001"/>
<display value="World"/>
</coding>
</jurisdiction>
<caseSensitive value="true"/>
<valueSet value="http://hl7.org/fhir/ValueSet/contact-point-use"/>
<content value="complete"/>
<concept>
<code value="home"/>
<display value="Home"/>
<definition value="A communication contact point at a home; attempted contacts for business purposes might intrude privacy and chances are one will contact family or other household members instead of the person one wishes to call. Typically used with urgent cases, or if no other contacts are available."/>
</concept>
<concept>
<code value="work"/>
<display value="Work"/>
<definition value="An office contact point. First choice for business related contacts during business hours."/>
</concept>
<concept>
<code value="temp"/>
<display value="Temp"/>
<definition value="A temporary contact point. The period can provide more detailed information."/>
</concept>
<concept>
<code value="old"/>
<display value="Old"/>
<definition value="This contact point is no longer in use (or was never correct, but retained for records)."/>
</concept>
<concept>
<code value="mobile"/>
<display value="Mobile"/>
<definition value="A telecommunication device that moves and stays with its owner. May have characteristics of all other use codes, suitable for urgent matters, not the first choice for routine business."/>
</concept>
</CodeSystem>

View File

@ -56,6 +56,8 @@ public class CliContext {
private HtmlInMarkdownCheck htmlInMarkdownCheck = HtmlInMarkdownCheck.WARNING; private HtmlInMarkdownCheck htmlInMarkdownCheck = HtmlInMarkdownCheck.WARNING;
@JsonProperty("allowDoubleQuotesInFHIRPath") @JsonProperty("allowDoubleQuotesInFHIRPath")
private boolean allowDoubleQuotesInFHIRPath = false; private boolean allowDoubleQuotesInFHIRPath = false;
@JsonProperty("disableDefaultResourceFetcher")
private boolean disableDefaultResourceFetcher = false;
@JsonProperty("checkIPSCodes") @JsonProperty("checkIPSCodes")
private boolean checkIPSCodes; private boolean checkIPSCodes;
@JsonProperty("langTransform") @JsonProperty("langTransform")
@ -328,6 +330,17 @@ public class CliContext {
this.allowDoubleQuotesInFHIRPath = allowDoubleQuotesInFHIRPath; this.allowDoubleQuotesInFHIRPath = allowDoubleQuotesInFHIRPath;
} }
@JsonProperty("disableDefaultResourceFetcher")
public boolean isDisableDefaultResourceFetcher() {
return disableDefaultResourceFetcher;
}
@JsonProperty("disableDefaultResourceFetcher")
public CliContext setDisableDefaultResourceFetcher(boolean disableDefaultResourceFetcher) {
this.disableDefaultResourceFetcher = disableDefaultResourceFetcher;
return this;
}
@JsonProperty("checkIPSCodes") @JsonProperty("checkIPSCodes")
public boolean isCheckIPSCodes() { public boolean isCheckIPSCodes() {
return checkIPSCodes; return checkIPSCodes;

View File

@ -524,9 +524,11 @@ public class ValidationService {
validationEngine.setForPublication(cliContext.isForPublication()); validationEngine.setForPublication(cliContext.isForPublication());
validationEngine.setShowTimes(cliContext.isShowTimes()); validationEngine.setShowTimes(cliContext.isShowTimes());
validationEngine.setAllowExampleUrls(cliContext.isAllowExampleUrls()); validationEngine.setAllowExampleUrls(cliContext.isAllowExampleUrls());
StandAloneValidatorFetcher fetcher = new StandAloneValidatorFetcher(validationEngine.getPcm(), validationEngine.getContext(), validationEngine); if (!cliContext.isDisableDefaultResourceFetcher()) {
validationEngine.setFetcher(fetcher); StandAloneValidatorFetcher fetcher = new StandAloneValidatorFetcher(validationEngine.getPcm(), validationEngine.getContext(), validationEngine);
validationEngine.getContext().setLocator(fetcher); validationEngine.setFetcher(fetcher);
validationEngine.getContext().setLocator(fetcher);
}
validationEngine.getBundleValidationRules().addAll(cliContext.getBundleValidationRules()); validationEngine.getBundleValidationRules().addAll(cliContext.getBundleValidationRules());
validationEngine.setJurisdiction(CodeSystemUtilities.readCoding(cliContext.getJurisdiction())); validationEngine.setJurisdiction(CodeSystemUtilities.readCoding(cliContext.getJurisdiction()));
TerminologyCache.setNoCaching(cliContext.isNoInternalCaching()); TerminologyCache.setNoCaching(cliContext.isNoInternalCaching());

View File

@ -89,6 +89,7 @@ public class Params {
public static final String SRC_LANG = "-src-lang"; public static final String SRC_LANG = "-src-lang";
public static final String TGT_LANG = "-tgt-lang"; public static final String TGT_LANG = "-tgt-lang";
public static final String ALLOW_DOUBLE_QUOTES = "-allow-double-quotes-in-fhirpath"; public static final String ALLOW_DOUBLE_QUOTES = "-allow-double-quotes-in-fhirpath";
public static final String DISABLE_DEFAULT_RESOURCE_FETCHER = "-disable-default-resource-fetcher";
public static final String CHECK_IPS_CODES = "-check-ips-codes"; public static final String CHECK_IPS_CODES = "-check-ips-codes";
public static final String BEST_PRACTICE = "-best-practice"; public static final String BEST_PRACTICE = "-best-practice";
@ -270,6 +271,8 @@ public class Params {
cliContext.setNoExtensibleBindingMessages(true); cliContext.setNoExtensibleBindingMessages(true);
} else if (args[i].equals(ALLOW_DOUBLE_QUOTES)) { } else if (args[i].equals(ALLOW_DOUBLE_QUOTES)) {
cliContext.setAllowDoubleQuotesInFHIRPath(true); cliContext.setAllowDoubleQuotesInFHIRPath(true);
} else if (args[i].equals(DISABLE_DEFAULT_RESOURCE_FETCHER)) {
cliContext.setDisableDefaultResourceFetcher(true);
} else if (args[i].equals(CHECK_IPS_CODES)) { } else if (args[i].equals(CHECK_IPS_CODES)) {
cliContext.setCheckIPSCodes(true); cliContext.setCheckIPSCodes(true);
} else if (args[i].equals(NO_UNICODE_BIDI_CONTROL_CHARS)) { } else if (args[i].equals(NO_UNICODE_BIDI_CONTROL_CHARS)) {

View File

@ -10,8 +10,10 @@ import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.endsWith; import static org.mockito.ArgumentMatchers.endsWith;
import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.ArgumentMatchers.notNull;
import static org.mockito.ArgumentMatchers.startsWith; import static org.mockito.ArgumentMatchers.startsWith;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
@ -236,17 +238,43 @@ class ValidationServiceTest {
*/ */
@Test @Test
public void buildValidationEngineTest() throws IOException, URISyntaxException { public void buildValidationEngineTest() throws IOException, URISyntaxException {
final TimeTracker timeTracker = mock(TimeTracker.class);
final org.hl7.fhir.utilities.TimeTracker timeTracker = mock(org.hl7.fhir.utilities.TimeTracker.class);
final SimpleWorkerContext workerContext = mock(SimpleWorkerContext.class); final SimpleWorkerContext workerContext = mock(SimpleWorkerContext.class);
final ValidationEngine validationEngine = mock(ValidationEngine.class); final ValidationEngine mockValidationEngine = mock(ValidationEngine.class);
when(validationEngine.getContext()).thenReturn(workerContext); when(mockValidationEngine.getContext()).thenReturn(workerContext);
final ValidationEngine.ValidationEngineBuilder validationEngineBuilder = mock(ValidationEngine.ValidationEngineBuilder.class);; final ValidationEngine.ValidationEngineBuilder mockValidationEngineBuilder = mock(ValidationEngine.ValidationEngineBuilder.class);;
final ValidationService validationService = createFakeValidationService(mockValidationEngineBuilder, mockValidationEngine);
final ValidationService validationService = new ValidationService() { CliContext cliContext = new CliContext();
validationService.buildValidationEngine(cliContext, null, timeTracker);
verify(mockValidationEngine).setFetcher(notNull());
verify(mockValidationEngineBuilder).withUserAgent(eq("fhir/validator/" + VersionUtil.getVersion()));
}
@Test
public void buildValidationEngineDisableDefaultResourceFetcherTest() throws IOException, URISyntaxException {
final TimeTracker timeTracker = mock(TimeTracker.class);
final SimpleWorkerContext workerContext = mock(SimpleWorkerContext.class);
final ValidationEngine mockValidationEngine = mock(ValidationEngine.class);
when(mockValidationEngine.getContext()).thenReturn(workerContext);
final ValidationEngine.ValidationEngineBuilder mockValidationEngineBuilder = mock(ValidationEngine.ValidationEngineBuilder.class);;
final ValidationService validationService = createFakeValidationService(mockValidationEngineBuilder, mockValidationEngine);
CliContext cliContext = new CliContext();
cliContext.setDisableDefaultResourceFetcher(true);
validationService.buildValidationEngine(cliContext, null, timeTracker);
verify(mockValidationEngine, never()).setFetcher(any());
verify(mockValidationEngineBuilder).withUserAgent(eq("fhir/validator/" + VersionUtil.getVersion()));
}
private static ValidationService createFakeValidationService(ValidationEngine.ValidationEngineBuilder validationEngineBuilder, ValidationEngine validationEngine) {
return new ValidationService() {
@Override @Override
protected ValidationEngine.ValidationEngineBuilder getValidationEngineBuilder() { protected ValidationEngine.ValidationEngineBuilder getValidationEngineBuilder() {
when(validationEngineBuilder.withTHO(anyBoolean())).thenReturn(validationEngineBuilder); when(validationEngineBuilder.withTHO(anyBoolean())).thenReturn(validationEngineBuilder);
@ -268,11 +296,5 @@ class ValidationServiceTest {
//Don't care. Do nothing. //Don't care. Do nothing.
} }
}; };
CliContext cliContext = new CliContext();
validationService.buildValidationEngine(cliContext, null, timeTracker);
verify(validationEngineBuilder).withUserAgent(eq("fhir/validator/" + VersionUtil.getVersion()));
} }
} }

View File

@ -294,7 +294,7 @@
<dependency> <dependency>
<groupId>org.xerial</groupId> <groupId>org.xerial</groupId>
<artifactId>sqlite-jdbc</artifactId> <artifactId>sqlite-jdbc</artifactId>
<version>3.43.0.0</version> <version>3.45.1.0</version>
</dependency> </dependency>
<dependency> <dependency>