rework r4 RDF tests to skip fewer examples (#2159)

+ relative URL resolution in RDFParser
+ test non-DomainResources in RDFParserTest
~ factor/organize RDFParserTest
~ avoid EXTERNAL shapes (for now) in shex-java
- disable the bundle-response.json example as it is apparently malformed
+ added urn:-guard to PreResourceStateHl7Org.wereBack ID update
This commit is contained in:
Eric Prud'hommeaux 2021-01-17 01:15:39 +01:00 committed by GitHub
parent 8a0b0885f2
commit 8b338cd168
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 152 additions and 66 deletions

View File

@ -1260,13 +1260,15 @@ class ParserState<T> {
String versionId = elem.getMeta().getVersionId(); String versionId = elem.getMeta().getVersionId();
if (StringUtils.isBlank(elem.getIdElement().getIdPart())) { if (StringUtils.isBlank(elem.getIdElement().getIdPart())) {
// Resource has no ID // Resource has no ID
} else if (StringUtils.isNotBlank(versionId)) { } else if (!elem.getIdElement().getIdPart().startsWith("urn:")) {
if (StringUtils.isNotBlank(versionId)) {
elem.getIdElement().setValue(resourceName + "/" + elem.getIdElement().getIdPart() + "/_history/" + versionId); elem.getIdElement().setValue(resourceName + "/" + elem.getIdElement().getIdPart() + "/_history/" + versionId);
} else { } else {
elem.getIdElement().setValue(resourceName + "/" + elem.getIdElement().getIdPart()); elem.getIdElement().setValue(resourceName + "/" + elem.getIdElement().getIdPart());
} }
} }
} }
}
} }

View File

@ -29,6 +29,7 @@ import org.apache.commons.lang3.StringUtils;
import org.apache.jena.datatypes.xsd.XSDDatatype; import org.apache.jena.datatypes.xsd.XSDDatatype;
import org.apache.jena.rdf.model.*; import org.apache.jena.rdf.model.*;
import org.apache.jena.riot.Lang; import org.apache.jena.riot.Lang;
import org.apache.jena.riot.system.IRIResolver;
import org.apache.jena.vocabulary.RDF; import org.apache.jena.vocabulary.RDF;
import org.hl7.fhir.instance.model.api.*; import org.hl7.fhir.instance.model.api.*;
@ -171,10 +172,14 @@ public class RDFParser extends BaseParser {
if (!uriBase.endsWith("/")) { if (!uriBase.endsWith("/")) {
uriBase = uriBase + "/"; uriBase = uriBase + "/";
} }
String resourceUri = uriBase + resource.getIdElement().toUnqualified();
if (parentResource == null) { if (parentResource == null) {
if (!resource.getIdElement().toUnqualified().hasIdPart()) {
parentResource = rdfModel.getResource(null);
} else {
String resourceUri = IRIResolver.resolve(resource.getIdElement().toUnqualified().toString(), uriBase).toString();
parentResource = rdfModel.getResource(resourceUri); parentResource = rdfModel.getResource(resourceUri);
}
// If the resource already exists and has statements, return that existing resource. // If the resource already exists and has statements, return that existing resource.
if (parentResource != null && parentResource.listProperties().toList().size() > 0) { if (parentResource != null && parentResource.listProperties().toList().size() > 0) {
return parentResource; return parentResource;

View File

@ -9,15 +9,14 @@ import fr.inria.lille.shexjava.schema.parsing.GenParser;
import fr.inria.lille.shexjava.validation.RecursiveValidation; import fr.inria.lille.shexjava.validation.RecursiveValidation;
import fr.inria.lille.shexjava.validation.ValidationAlgorithm; import fr.inria.lille.shexjava.validation.ValidationAlgorithm;
import org.apache.commons.rdf.api.Graph; import org.apache.commons.rdf.api.Graph;
import org.apache.commons.rdf.api.IRI; import org.apache.commons.rdf.api.RDFTerm;
import org.apache.commons.rdf.rdf4j.RDF4J; import org.apache.commons.rdf.rdf4j.RDF4J;
import org.eclipse.rdf4j.model.Model; import org.eclipse.rdf4j.model.Model;
import org.eclipse.rdf4j.model.impl.SimpleIRI;
import org.eclipse.rdf4j.rio.RDFFormat; import org.eclipse.rdf4j.rio.RDFFormat;
import org.eclipse.rdf4j.rio.Rio; import org.eclipse.rdf4j.rio.Rio;
import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.model.Base; import org.hl7.fhir.r4.model.Base;
import org.hl7.fhir.r4.model.DomainResource; import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.MethodSource;
@ -25,8 +24,10 @@ import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver; import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver; import org.springframework.core.io.support.ResourcePatternResolver;
import java.io.ByteArrayInputStream;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader; import java.io.StringReader;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
@ -53,6 +54,15 @@ public class RDFParserTest extends BaseTest {
fhirSchema = GenParser.parseSchema(schemaFile, Collections.emptyList()); fhirSchema = GenParser.parseSchema(schemaFile, Collections.emptyList());
} }
// If we can't round-trip JSON, we skip the Turtle round-trip test.
private static ArrayList<String> jsonRoundTripErrors = new ArrayList<String>();
@AfterAll
static void reportJsonRoundTripErrors() {
System.out.println(jsonRoundTripErrors.size() + " tests disqualified because of JSON round-trip errors");
for (String e : jsonRoundTripErrors)
System.out.println(e);
}
/** /**
* This test method has a method source for each JSON file in the resources/rdf-test-input directory (see #getInputFiles). * This test method has a method source for each JSON file in the resources/rdf-test-input directory (see #getInputFiles).
* Each input file is expected to be a JSON representation of an R4 FHIR resource. * Each input file is expected to be a JSON representation of an R4 FHIR resource.
@ -62,35 +72,124 @@ public class RDFParserTest extends BaseTest {
* 3. Perform a graph validation on the resulting RDF using ShEx and ShEx-java -- ensure validation passed * 3. Perform a graph validation on the resulting RDF using ShEx and ShEx-java -- ensure validation passed
* 4. Parse the RDF string into the HAPI object model -- ensure resource instance is not null * 4. Parse the RDF string into the HAPI object model -- ensure resource instance is not null
* 5. Perform deep equals comparison of JSON-originated instance and RDF-originated instance -- ensure equality * 5. Perform deep equals comparison of JSON-originated instance and RDF-originated instance -- ensure equality
* @param inputFile -- path to resource file to be tested * @param referenceFilePath -- path to resource file to be tested
* @throws IOException -- thrown when parsing RDF string into graph model * @throws IOException -- thrown when parsing RDF string into graph model
*/ */
@ParameterizedTest @ParameterizedTest
@MethodSource("getInputFiles") @MethodSource("getInputFiles")
public void testRDFRoundTrip(String inputFile) throws IOException { public void testRDFRoundTrip(String referenceFilePath) throws IOException {
FileInputStream inputStream = new FileInputStream(inputFile); String referenceFileName = referenceFilePath.substring(referenceFilePath.lastIndexOf("/")+1);
IBaseResource resource; IBaseResource referenceResource = parseJson(new FileInputStream(referenceFilePath));
String resourceType; String referenceJson = serializeJson(ourCtx, referenceResource);
// Parse JSON input as Resource
resource = ourCtx.newJsonParser().parseResource(inputStream);
assertNotNull(resource);
resourceType = resource.fhirType();
// Write the resource out to an RDF String
String rdfContent = ourCtx.newRDFParser().encodeResourceToString(resource);
assertNotNull(rdfContent);
// Perform ShEx validation on RDF // Perform ShEx validation on RDF
String turtleString = serializeRdf(ourCtx, referenceResource);
validateRdf(turtleString, referenceFileName, referenceResource);
// If we can round-trip JSON
IBaseResource viaJsonResource = parseJson(new ByteArrayInputStream(referenceJson.getBytes()));
if (((Base)viaJsonResource).equalsDeep((Base)referenceResource)) {
// Parse RDF content as resource
IBaseResource viaTurtleResource = parseRdf(ourCtx, new StringReader(turtleString));
assertNotNull(viaTurtleResource);
// Compare original JSON-based resource against RDF-based resource
String viaTurtleJson = serializeJson(ourCtx, viaTurtleResource);
if (!((Base)viaTurtleResource).equalsDeep((Base)referenceResource)) {
String failMessage = referenceFileName + ": failed to round-trip Turtle ";
if (referenceJson.equals(viaTurtleJson))
throw new Error(failMessage
+ "\nttl: " + turtleString
+ "\nexp: " + referenceJson);
else
assertEquals(referenceJson, viaTurtleJson, failMessage + "\nttl: " + turtleString);
}
} else {
String gotString = serializeJson(ourCtx, viaJsonResource);
String skipMessage = referenceFileName + ": failed to round-trip JSON" +
(referenceJson.equals(gotString)
? "\ngot: " + gotString + "\nexp: " + referenceJson
: "\nsome inequality not visible in: " + referenceJson);
System.out.println(referenceFileName + " skipped");
// Specific messages are printed at end of run.
jsonRoundTripErrors.add(skipMessage);
}
}
private static Stream<String> getInputFiles() throws IOException {
ClassLoader cl = RDFParserTest.class.getClassLoader();
List<String> resourceList = new ArrayList<>();
ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(cl);
Resource[] resources = resolver.getResources("classpath:rdf-test-input/*.json") ;
for (Resource resource: resources)
resourceList.add(resource.getFile().getPath());
return resourceList.stream();
}
// JSON functions
public IBaseResource parseJson(InputStream inputStream) {
IParser refParser = ourCtx.newJsonParser();
refParser.setStripVersionsFromReferences(false);
// parser.setDontStripVersionsFromReferencesAtPaths();
IBaseResource ret = refParser.parseResource(inputStream);
assertNotNull(ret);
return ret;
}
public String serializeJson(FhirContext ctx, IBaseResource resource) {
IParser jsonParser = ctx.newJsonParser();
jsonParser.setStripVersionsFromReferences(false);
String ret = jsonParser.encodeResourceToString(resource);
assertNotNull(ret);
return ret;
}
// Rdf (Turtle) functions
public IBaseResource parseRdf(FhirContext ctx, StringReader inputStream) {
IParser refParser = ctx.newRDFParser();
IBaseResource ret = refParser.parseResource(inputStream);
assertNotNull(ret);
return ret;
}
public String serializeRdf(FhirContext ctx, IBaseResource resource) {
IParser rdfParser = ourCtx.newRDFParser();
rdfParser.setStripVersionsFromReferences(false);
rdfParser.setServerBaseUrl("http://a.example/fhir/");
String ret = rdfParser.encodeResourceToString(resource);
assertNotNull(ret);
return ret;
}
public void validateRdf(String rdfContent, String referenceFileName, IBaseResource referenceResource) throws IOException {
String baseIRI = "http://a.example/shex/";
RDF4J factory = new RDF4J(); RDF4J factory = new RDF4J();
GlobalFactory.RDFFactory = factory; //set the global factory used in shexjava GlobalFactory.RDFFactory = factory; //set the global factory used in shexjava
// load the model
String baseIRI = "http://a.example.shex/";
Model data = Rio.parse(new StringReader(rdfContent), baseIRI, RDFFormat.TURTLE); Model data = Rio.parse(new StringReader(rdfContent), baseIRI, RDFFormat.TURTLE);
FixedShapeMapEntry fixedMapEntry = new FixedShapeMapEntry(factory, data, referenceResource.fhirType(), baseIRI);
Graph dataGraph = factory.asGraph(data); // create the graph
ValidationAlgorithm validation = new RecursiveValidation(fhirSchema, dataGraph);
validation.validate(fixedMapEntry.node, fixedMapEntry.shape);
boolean result = validation.getTyping().isConformant(fixedMapEntry.node, fixedMapEntry.shape);
assertTrue(result,
referenceFileName + ": failed to validate " + fixedMapEntry
+ "\n" + referenceFileName
+ "\n" + rdfContent
);
}
// Shape Expressions functions
class FixedShapeMapEntry {
RDFTerm node;
Label shape;
FixedShapeMapEntry(RDF4J factory, Model data, String resourceType, String baseIRI) {
String rootSubjectIri = null; String rootSubjectIri = null;
// StmtIterator i = data.listStatements();
for (org.eclipse.rdf4j.model.Resource resourceStream : data.subjects()) { for (org.eclipse.rdf4j.model.Resource resourceStream : data.subjects()) {
if (resourceStream instanceof SimpleIRI) { // if (resourceStream instanceof SimpleIRI) {
Model filteredModel = data.filter(resourceStream, factory.getValueFactory().createIRI(NODE_ROLE_IRI), factory.getValueFactory().createIRI(TREE_ROOT_IRI), (org.eclipse.rdf4j.model.Resource)null); Model filteredModel = data.filter(resourceStream, factory.getValueFactory().createIRI(NODE_ROLE_IRI), factory.getValueFactory().createIRI(TREE_ROOT_IRI), (org.eclipse.rdf4j.model.Resource)null);
if (filteredModel != null && filteredModel.subjects().size() == 1) { if (filteredModel != null && filteredModel.subjects().size() == 1) {
Optional<org.eclipse.rdf4j.model.Resource> rootResource = filteredModel.subjects().stream().findFirst(); Optional<org.eclipse.rdf4j.model.Resource> rootResource = filteredModel.subjects().stream().findFirst();
@ -100,46 +199,21 @@ public class RDFParserTest extends BaseTest {
} }
} }
// }
} }
}
// create the graph
Graph dataGraph = factory.asGraph(data);
// choose focus node and shapelabel // choose focus node and shapelabel
IRI focusNode = factory.createIRI(rootSubjectIri); this.node = rootSubjectIri.indexOf(":") == -1
? factory.createBlankNode(rootSubjectIri)
: factory.createIRI(rootSubjectIri);
Label shapeLabel = new Label(factory.createIRI(FHIR_SHAPE_PREFIX + resourceType)); Label shapeLabel = new Label(factory.createIRI(FHIR_SHAPE_PREFIX + resourceType));
// this.node = focusNode;
ValidationAlgorithm validation = new RecursiveValidation(fhirSchema, dataGraph); this.shape = shapeLabel;
validation.validate(focusNode, shapeLabel);
boolean result = validation.getTyping().isConformant(focusNode, shapeLabel);
assertTrue(result);
// Parse RDF content as resource
IBaseResource parsedResource = ourCtx.newRDFParser().parseResource(new StringReader(rdfContent));
assertNotNull(parsedResource);
// Compare original JSON-based resource against RDF-based resource
if (parsedResource instanceof DomainResource) {
// This is a hack because this initializes the collection if it is empty
((DomainResource) parsedResource).getContained();
boolean deepEquals = ((Base)parsedResource).equalsDeep((Base)resource);
assertTrue(deepEquals);
} else {
ourLog.warn("Input JSON did not yield a DomainResource");
}
} }
private static Stream<String> getInputFiles() throws IOException { public String toString() {
ClassLoader cl = RDFParserTest.class.getClassLoader(); return "<" + node.toString() + ">@" + shape.toPrettyString();
List<String> resourceList = new ArrayList<>();
ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(cl);
Resource[] resources = resolver.getResources("classpath:rdf-test-input/*.json") ;
for (Resource resource: resources){
resourceList.add(resource.getFile().getPath());
} }
return resourceList.stream();
} }
} }

View File

@ -0,0 +1,5 @@
# Why is this disabled?
| file | reason |
| - | - |
| [bundle-response](bundle-response.json) | [entry\[0\].response.outcome.issue](bundle-response.json#L50-L61) is not in [R4 Resource](http://hl7.org/fhir/resource.html#Resource) |

View File

@ -13267,7 +13267,7 @@ fhirvs:contact-point-use ["home" "work" "temp" "old" "mobile"]
fhirvs:immunization-status ["completed" "entered-in-error" "not-done"] fhirvs:immunization-status ["completed" "entered-in-error" "not-done"]
# This value set includes all possible codes from BCP-13 (http://tools.ietf.org/html/bcp13) # This value set includes all possible codes from BCP-13 (http://tools.ietf.org/html/bcp13)
fhirvs:mimetypes EXTERNAL fhirvs:mimetypes .
# The use of an address. # The use of an address.
fhirvs:address-use ["home" "work" "temp" "old" "billing"] fhirvs:address-use ["home" "work" "temp" "old" "billing"]