diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/conformance/AdditionalBindingsRenderer.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/conformance/AdditionalBindingsRenderer.java index 26aeb0a33..fbcd2eadd 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/conformance/AdditionalBindingsRenderer.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/conformance/AdditionalBindingsRenderer.java @@ -323,48 +323,48 @@ public class AdditionalBindingsRenderer { boolean r5 = context == null || context.getWorker() == null ? false : VersionUtilities.isR5Plus(context.getWorker().getVersion()); switch (purpose) { case "maximum": - td.ah(r5 ? "valueset-additional-binding-purpose.html#additional-binding-purpose-maximum" : corePath+"extension-elementdefinition-maxvalueset.html", "A required binding, for use when the binding strength is 'extensible' or 'preferred'").tx("Max Binding"); + td.ah(r5 ? corePath+"valueset-additional-binding-purpose.html#additional-binding-purpose-maximum" : corePath+"extension-elementdefinition-maxvalueset.html", "A required binding, for use when the binding strength is 'extensible' or 'preferred'").tx("Max Binding"); break; case "minimum": - td.ah(r5 ? "valueset-additional-binding-purpose.html#additional-binding-purpose-minimum" : corePath+"extension-elementdefinition-minvalueset.html", "The minimum allowable value set - any conformant system SHALL support all these codes").tx("Min Binding"); + td.ah(r5 ? corePath+"valueset-additional-binding-purpose.html#additional-binding-purpose-minimum" : corePath+"extension-elementdefinition-minvalueset.html", "The minimum allowable value set - any conformant system SHALL support all these codes").tx("Min Binding"); break; case "required" : - td.ah(r5 ? "valueset-additional-binding-purpose.html#additional-binding-purpose-required" : corePath+"terminologies.html#strength", "Validators will check this binding (strength = required)").tx("Required Binding"); + td.ah(r5 ? corePath+"valueset-additional-binding-purpose.html#additional-binding-purpose-required" : corePath+"terminologies.html#strength", "Validators will check this binding (strength = required)").tx("Required Binding"); break; case "extensible" : - td.ah(r5 ? "valueset-additional-binding-purpose.html#additional-binding-purpose-extensible" : corePath+"terminologies.html#strength", "Validators will check this binding (strength = extensible)").tx("Extensible Binding"); + td.ah(r5 ? corePath+"valueset-additional-binding-purpose.html#additional-binding-purpose-extensible" : corePath+"terminologies.html#strength", "Validators will check this binding (strength = extensible)").tx("Extensible Binding"); break; case "current" : if (r5) { - td.ah(r5 ? "valueset-additional-binding-purpose.html#additional-binding-purpose-current" : corePath+"terminologies.html#strength", "New records are required to use this value set, but legacy records may use other codes").tx("Current Binding"); + td.ah(r5 ? corePath+"valueset-additional-binding-purpose.html#additional-binding-purpose-current" : corePath+"terminologies.html#strength", "New records are required to use this value set, but legacy records may use other codes").tx("Current Binding"); } else { td.span(null, "New records are required to use this value set, but legacy records may use other codes").tx("Required"); } break; case "preferred" : if (r5) { - td.ah(r5 ? "valueset-additional-binding-purpose.html#additional-binding-purpose-preferred" : corePath+"terminologies.html#strength", "This is the value set that is recommended (documentation should explain why)").tx("Preferred Binding"); + td.ah(r5 ? corePath+"valueset-additional-binding-purpose.html#additional-binding-purpose-preferred" : corePath+"terminologies.html#strength", "This is the value set that is recommended (documentation should explain why)").tx("Preferred Binding"); } else { td.span(null, "This is the value set that is recommended (documentation should explain why)").tx("Recommended"); } break; case "ui" : if (r5) { - td.ah(r5 ? "valueset-additional-binding-purpose.html#additional-binding-purpose-ui" : corePath+"terminologies.html#strength", "This value set is provided to user look up in a given context").tx("UI Binding"); + td.ah(r5 ? corePath+"valueset-additional-binding-purpose.html#additional-binding-purpose-ui" : corePath+"terminologies.html#strength", "This value set is provided to user look up in a given context").tx("UI Binding"); } else { td.span(null, "This value set is provided to user look up in a given context").tx("UI"); } break; case "starter" : if (r5) { - td.ah(r5 ? "valueset-additional-binding-purpose.html#additional-binding-purpose-starter" : corePath+"terminologies.html#strength", "This value set is a good set of codes to start with when designing your system").tx("Starter Set"); + td.ah(r5 ? corePath+"valueset-additional-binding-purpose.html#additional-binding-purpose-starter" : corePath+"terminologies.html#strength", "This value set is a good set of codes to start with when designing your system").tx("Starter Set"); } else { td.span(null, "This value set is a good set of codes to start with when designing your system").tx("Starter"); } break; case "component" : if (r5) { - td.ah(r5 ? "valueset-additional-binding-purpose.html#additional-binding-purpose-component" : corePath+"terminologies.html#strength", "This value set is a component of the base value set").tx("Component"); + td.ah(r5 ? corePath+"valueset-additional-binding-purpose.html#additional-binding-purpose-component" : corePath+"terminologies.html#strength", "This value set is a component of the base value set").tx("Component"); } else { td.span(null, "This value set is a component of the base value set").tx("Component"); } diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/elementmodel/ParserBase.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/elementmodel/ParserBase.java index af71284a5..39b77e24f 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/elementmodel/ParserBase.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/elementmodel/ParserBase.java @@ -243,8 +243,9 @@ public abstract class ParserBase { return logical; } - public void setLogical(StructureDefinition logical) { + public ParserBase setLogical(StructureDefinition logical) { this.logical = logical; + return this; } } \ No newline at end of file diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/structuremap/StructureMapUtilities.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/structuremap/StructureMapUtilities.java index 9f66c0e50..727546b2a 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/structuremap/StructureMapUtilities.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/structuremap/StructureMapUtilities.java @@ -2761,4 +2761,30 @@ public class StructureMapUtilities { return null; } + public ResolvedGroup getGroupForUrl(StructureMap map, String url, StructureMapInputMode mode) { + for (StructureMapGroupComponent grp : map.getGroup()) { + if (grp.getTypeMode() != StructureMapGroupTypeMode.NULL) { + for (StructureMapGroupInputComponent p : grp.getInput()) { + if (mode == null || mode == p.getMode()) { + String t = resolveInputType(p, map); + if (url.equals(t)) { + return new ResolvedGroup(map, grp); + } + } + } + } + } + return null; + } + + public String getInputType(ResolvedGroup grp, StructureMapInputMode mode) { + if (grp.getTargetGroup().getInput().size() != 2 || grp.getTargetGroup().getInput().get(0).getMode() == grp.getTargetGroup().getInput().get(1).getMode()) { + return null; + } else if (grp.getTargetGroup().getInput().get(0).getMode() == mode) { + return resolveInputType(grp.getTargetGroup().getInput().get(0), grp.getTargetMap()); + } else { + return resolveInputType(grp.getTargetGroup().getInput().get(1), grp.getTargetMap()); + } + } + } \ No newline at end of file diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ValidatorUtils.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ValidatorUtils.java index 3554d5e0a..54b24f857 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ValidatorUtils.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ValidatorUtils.java @@ -47,7 +47,7 @@ public class ValidatorUtils { } } - protected static IWorkerContext.IContextResourceLoader loaderForVersion(String version) { + public static IWorkerContext.IContextResourceLoader loaderForVersion(String version) { if (Utilities.noString(version)) { return null; } diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/special/R4R5MapTester.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/special/R4R5MapTester.java index cff739b6a..32554b576 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/special/R4R5MapTester.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/special/R4R5MapTester.java @@ -1,26 +1,96 @@ package org.hl7.fhir.validation.special; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.FileOutputStream; import java.io.IOException; +import java.io.InputStream; +import java.util.HashSet; import java.util.List; +import java.util.Set; import org.hl7.fhir.r5.model.Enumerations.PublicationStatus; import org.hl7.fhir.r5.model.StructureDefinition; +import org.hl7.fhir.r5.model.StructureDefinition.StructureDefinitionKind; import org.hl7.fhir.r5.model.StructureMap; import org.hl7.fhir.r5.model.StructureMap.StructureMapInputMode; +import org.hl7.fhir.convertors.loaders.loaderR5.R4ToR5Loader; +import org.hl7.fhir.exceptions.DefinitionException; +import org.hl7.fhir.exceptions.FHIRException; +import org.hl7.fhir.exceptions.FHIRFormatError; +import org.hl7.fhir.r4.formats.IParser.OutputStyle; +import org.hl7.fhir.r4.model.Resource; +import org.hl7.fhir.r4.model.ResourceFactory; +import org.hl7.fhir.r4.test.utils.TestingUtilities; import org.hl7.fhir.r5.context.SimpleWorkerContext; +import org.hl7.fhir.r5.context.IWorkerContext.IContextResourceLoader; import org.hl7.fhir.r5.context.SimpleWorkerContext.SimpleWorkerContextBuilder; +import org.hl7.fhir.r5.elementmodel.Element; +import org.hl7.fhir.r5.utils.structuremap.ResolvedGroup; import org.hl7.fhir.r5.utils.structuremap.StructureMapUtilities; import org.hl7.fhir.r5.utils.structuremap.VariableMode; +import org.hl7.fhir.utilities.TextFile; import org.hl7.fhir.utilities.Utilities; +import org.hl7.fhir.utilities.VersionUtilities; import org.hl7.fhir.utilities.json.JsonException; +import org.hl7.fhir.utilities.json.model.JsonElement; import org.hl7.fhir.utilities.json.model.JsonObject; import org.hl7.fhir.utilities.json.model.JsonProperty; import org.hl7.fhir.utilities.json.parser.JsonParser; import org.hl7.fhir.utilities.npm.FilesystemPackageCacheManager; +import org.hl7.fhir.utilities.npm.NpmPackage; +import org.hl7.fhir.validation.IgLoader; +import org.hl7.fhir.validation.ValidatorUtils; +import org.hl7.fhir.validation.special.R4R5MapTester.Stats; public class R4R5MapTester { + public class Stats { + + private Set errors = new HashSet<>(); + private int total; + private int parsed; + private int forward; + + public void example() { + total++; + } + + public void parsed() { + parsed++; + } + + public void error(String s) { + errors.add(s); + } + + public void forward() { + forward++; + } + + public int totalCount() { + return total; + } + + public int parseCount() { + return parsed; + } + + public String summary() { + if (errors.size() == 0) { + return "All OK"; + } else { + return String.join(", ", errors); + } + } + + public boolean ok() { + return errors.size() == 0; + } + } + + private boolean saveProcess = false; + private SimpleWorkerContext context; private FilesystemPackageCacheManager pcm; private StructureMapUtilities utils; @@ -32,18 +102,29 @@ public class R4R5MapTester { new R4R5MapTester().testMaps(args[0]); } - private void testMaps(String src) throws JsonException, IOException { + public void testMaps(String src) throws JsonException, IOException { log("Load Test Outcomes"); JsonObject json = JsonParser.parseObjectFromFile(Utilities.path(src, "input", "_data", "conversions.json")); log("Load R5"); pcm = new FilesystemPackageCacheManager(true); context = new SimpleWorkerContextBuilder().withAllowLoadingDuplicates(true).fromPackage(pcm.loadPackage("hl7.fhir.r5.core#current")); log("Load Maps"); - context.loadFromPackage(pcm.loadPackage("hl7.fhir.uv.extensions#dev"), null); +// context.loadFromPackage(pcm.loadPackage(), null); + + loadPackage("hl7.fhir.uv.extensions#dev"); + loadPackage("hl7.fhir.r4.core#4.0.1"); + loadPackage("hl7.fhir.r4b.core#4.3.0"); + + log("Load R4 Examples"); + NpmPackage r4Examples = pcm.loadPackage("hl7.fhir.r4.examples"); + log("Load R4B Examples"); + NpmPackage r4bExamples = pcm.loadPackage("hl7.fhir.r4b.examples"); + + utils = new StructureMapUtilities(context); allMaps = context.fetchResourcesByType(StructureMap.class); - log("Resource Count = "+context.getResourceNames().size()); + log("Go. "+context.getResourceNames().size()+" types of resources"); log("Map Count = "+allMaps.size()); boolean changed = false; for (JsonProperty jp : json.getProperties()) { @@ -53,8 +134,8 @@ public class R4R5MapTester { StructureDefinition sd = context.fetchTypeDefinition(rn); List mapSrc = utils.getMapsForUrl(allMaps, sd.getUrl(), StructureMapInputMode.SOURCE); List mapTgt = utils.getMapsForUrl(allMaps, sd.getUrl(), StructureMapInputMode.TARGET); - changed = checkMaps(rn, o.getJsonObject("r4"), "http://hl7.org/fhir/4.0", mapSrc, mapTgt) || changed; - changed = checkMaps(rn, o.getJsonObject("r4b"), "http://hl7.org/fhir/4.0", mapSrc, mapTgt) || changed; + changed = checkMaps(sd, o.getJsonObject("r4"), "http://hl7.org/fhir/4.0", mapSrc, mapTgt, r4Examples) || changed; + changed = checkMaps(sd, o.getJsonObject("r4b"), "http://hl7.org/fhir/4.3", mapSrc, mapTgt, r4bExamples) || changed; } if (changed) { JsonParser.compose(json, new FileOutputStream(Utilities.path(src, "input", "_data", "conversions.json")), true); @@ -76,7 +157,15 @@ public class R4R5MapTester { } - private boolean checkMaps(String rn, JsonObject json, String ns, List mapSrc, List mapTgt) { + private void loadPackage(String pid) throws FHIRException, IOException { + log("Load "+pid); + NpmPackage npm = pcm.loadPackage(pid); + IContextResourceLoader loader = ValidatorUtils.loaderForVersion(npm.fhirVersion()); + loader.setPatchUrls(VersionUtilities.isCorePackage(npm.id())); + int count = context.loadFromPackage(npm, loader); + } + + private boolean checkMaps(StructureDefinition sd, JsonObject json, String ns, List mapSrc, List mapTgt, NpmPackage examples) throws IOException { List src = utils.getMapsForUrlPrefix(mapSrc, ns, StructureMapInputMode.TARGET); List tgt = utils.getMapsForUrlPrefix(mapTgt, ns, StructureMapInputMode.SOURCE); if (src.size() + tgt.size() == 0) { @@ -92,9 +181,30 @@ public class R4R5MapTester { isDraft = map.getStatus() == PublicationStatus.DRAFT || isDraft; } json.set("status", ""+(src.size()+tgt.size())+" Maps Defined"+(isDraft ? " (draft)" : "")); - if (context.getResourceNames().contains(rn)) { - json.set("testColor", "#ffcccc"); - json.set("testMessage", "To Do"); + json.set("testColor", "#ffcccc"); + if (sd.getKind() == StructureDefinitionKind.RESOURCE) { + if (tgt.size() == 1 && src.size() == 1) { + StructureMap tgtM = tgt.get(0); + ResolvedGroup tgtG = utils.getGroupForUrl(tgtM, sd.getUrl(), StructureMapInputMode.TARGET); + String tgtU = utils.getInputType(tgtG, StructureMapInputMode.SOURCE); + assert tgtU.startsWith(ns); + StructureMap srcM = src.get(0); + ResolvedGroup srcG = utils.getGroupForUrl(srcM, sd.getUrl(), StructureMapInputMode.SOURCE); + String srcU = utils.getInputType(srcG, StructureMapInputMode.TARGET); + assert srcU.startsWith(ns); + if (!srcU.equals(tgtU)) { + json.set("testMessage", "Maps do not round trip to same resource ("+Utilities.tail(srcU)+" -> "+Utilities.tail(tgtU)+") - can't test"); + } else { + StructureDefinition tsd = context.fetchResource(StructureDefinition.class, srcU); + if (tsd == null) { + json.set("testMessage", "Undefined type "+srcU); + } else { + testRoundTrips(sd, json, tgtG, srcG, tsd, examples); + } + } + } else { + json.set("testMessage", "Multiple matching maps ("+src.size()+"/"+tgt.size()+") - no tests performed"); + } } else { json.set("testColor", "#eeeeee"); json.set("testMessage", "n/a"); @@ -103,6 +213,44 @@ public class R4R5MapTester { return true; } + private void testRoundTrips(StructureDefinition sd, JsonObject json, ResolvedGroup tgtG, ResolvedGroup srcG, StructureDefinition tsd, NpmPackage examples) throws IOException { + + Stats stats = new Stats(); + for (String s : examples.listResources(tsd.getType())) { + log(" Test "+examples.id()+"::"+s); + try { + testRoundTrip(sd, tsd, tgtG, srcG, stats, examples.load("package", s)); + } catch (Exception e) { + log("error: "+e.getMessage()); + stats.error("Error: "+e.getMessage()); + } + } + json.set("total", stats.totalCount()); + json.set("parsed", stats.parseCount()); + json.set("testMessage", stats.summary()); + if (stats.ok()) { + json.set("testColor", "#d4ffdf"); + } + } + + private void testRoundTrip(StructureDefinition sd, StructureDefinition tsd, ResolvedGroup tgtG, ResolvedGroup srcG, Stats stats, InputStream stream) throws FHIRFormatError, DefinitionException, FHIRException, IOException { + stats.example(); + Element r4 = new org.hl7.fhir.r5.elementmodel.JsonParser(context).setLogical(tsd).parseSingle(stream); + stats.parsed(); + String id = r4.getIdBase(); + checkSave(id, "src.loaded", r4); + + Resource r5 = ResourceFactory.createResource(sd.getType()); + utils.transform(context, r4, tgtG.getTargetMap(), r4); + stats.forward(); + } + + private void checkSave(String id, String state, Element e) { + if (saveProcess) { +// new org.hl7.fhir.r4.elementmodel.JsonParser(context).compose(r3, bso, OutputStyle.PRETTY, null); + } + } + private void log(String msg) { System.out.println(msg); } diff --git a/pom.xml b/pom.xml index f3db74e82..384962591 100644 --- a/pom.xml +++ b/pom.xml @@ -19,7 +19,7 @@ 6.2.1 - 1.2.20 + 1.2.21-SNAPSHOT 5.7.1 1.8.2 3.0.0-M5