From 5cc4e3ef03075f3e05830fe2316ba888730de9df Mon Sep 17 00:00:00 2001 From: Grahame Grieve Date: Mon, 4 Sep 2023 13:38:22 +1000 Subject: [PATCH] rework the way intermediary content is handled in the validator + fix impose-profile handling for IPS-AU --- .../hl7/fhir/r5/elementmodel/FmlParser.java | 2 +- .../hl7/fhir/r5/elementmodel/JsonParser.java | 2 +- .../hl7/fhir/r5/elementmodel/ParserBase.java | 12 +++-- .../hl7/fhir/r5/elementmodel/SHCParser.java | 4 +- .../hl7/fhir/r5/elementmodel/SHLParser.java | 17 ++++--- .../fhir/r5/elementmodel/TurtleParser.java | 2 +- .../r5/elementmodel/VerticalBarParser.java | 2 +- .../hl7/fhir/r5/elementmodel/XmlParser.java | 2 +- .../instance/InstanceValidator.java | 46 +++++++++++++++++++ 9 files changed, 72 insertions(+), 17 deletions(-) diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/elementmodel/FmlParser.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/elementmodel/FmlParser.java index 87bbf5f0d..73a50c6c1 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/elementmodel/FmlParser.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/elementmodel/FmlParser.java @@ -50,7 +50,7 @@ public class FmlParser extends ParserBase { ByteArrayInputStream stream = new ByteArrayInputStream(content); String text = TextFile.streamToString(stream); List result = new ArrayList<>(); - NamedElement ctxt = new NamedElement("fml", content); + NamedElement ctxt = new NamedElement("focus", "fml", content); ctxt.setElement(parse(ctxt.getErrors(), text)); result.add(ctxt); return result; diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/elementmodel/JsonParser.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/elementmodel/JsonParser.java index 770dd311d..4f3084af1 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/elementmodel/JsonParser.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/elementmodel/JsonParser.java @@ -119,7 +119,7 @@ public class JsonParser extends ParserBase { @Override public List parse(InputStream inStream) throws IOException, FHIRException { byte[] content = TextFile.streamToBytes(inStream); - NamedElement ctxt = new NamedElement("json", content); + NamedElement ctxt = new NamedElement("focus", "json", content); ByteArrayInputStream stream = new ByteArrayInputStream(content); 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 572d5ebea..344afc760 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 @@ -71,21 +71,24 @@ public abstract class ParserBase { public class NamedElement { private String name; + private String extension; private Element element; private byte[] content; private List errors = new ArrayList<>(); - public NamedElement(String name, Element element, byte[] content) { + public NamedElement(String name, String extension, Element element, byte[] content) { super(); this.name = name; this.element = element; this.content = content; + this.extension = extension; } - public NamedElement(String name, byte[] content) { + public NamedElement(String name, String extension, byte[] content) { super(); this.name = name; this.content = content; + this.extension = extension; } public String getName() { @@ -106,7 +109,10 @@ public abstract class ParserBase { public void setElement(Element element) { this.element = element; - + } + + public String getFilename() { + return name+"."+extension; } } diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/elementmodel/SHCParser.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/elementmodel/SHCParser.java index 5193c4a1c..0760a2531 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/elementmodel/SHCParser.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/elementmodel/SHCParser.java @@ -62,7 +62,7 @@ public class SHCParser extends ParserBase { byte[] content = TextFile.streamToBytes(inStream); ByteArrayInputStream stream = new ByteArrayInputStream(content); List res = new ArrayList<>(); - NamedElement shc = new NamedElement("shc", content); + NamedElement shc = new NamedElement("shc", "json", content); res.add(shc); String src = TextFile.streamToString(stream).trim(); @@ -146,7 +146,7 @@ public class SHCParser extends ParserBase { return res; } // ok. all checks passed, we can now validate the bundle - NamedElement bnd = new NamedElement(path, org.hl7.fhir.utilities.json.parser.JsonParser.composeBytes(cs.getJsonObject("fhirBundle"))); + NamedElement bnd = new NamedElement(path, "json", org.hl7.fhir.utilities.json.parser.JsonParser.composeBytes(cs.getJsonObject("fhirBundle"))); res.add(bnd); bnd.setElement(jsonParser.parse(bnd.getErrors(), cs.getJsonObject("fhirBundle"))); } diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/elementmodel/SHLParser.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/elementmodel/SHLParser.java index ca2200812..2ed4b8e07 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/elementmodel/SHLParser.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/elementmodel/SHLParser.java @@ -76,7 +76,7 @@ public class SHLParser extends ParserBase { byte[] content = TextFile.streamToBytes(inStream); List res = new ArrayList<>(); - NamedElement shl = addNamedElement(res, "shl", content); + NamedElement shl = addNamedElement(res, "shl", "txt", content); String src = TextFile.bytesToString(content); if (src.startsWith("shlink:/")) { @@ -92,8 +92,8 @@ public class SHLParser extends ParserBase { src = null; } if (src != null) { - NamedElement json = addNamedElement(res, "json", TextFile.stringToBytes(src, false)); byte[] cntin = Base64.getUrlDecoder().decode(src); + NamedElement json = addNamedElement(res, "json", "json", cntin); JsonObject j = null; try { j = org.hl7.fhir.utilities.json.parser.JsonParser.parseObject(cntin); @@ -146,7 +146,7 @@ public class SHLParser extends ParserBase { private void checkManifest(List res, HTTPResult cnt) throws IOException { - NamedElement manifest = addNamedElement(res, "manifest", cnt.getContent()); + NamedElement manifest = addNamedElement(res, "manifest", "json", cnt.getContent()); if (!cnt.getContentType().equals("application/json")) { logError(manifest.getErrors(), "202-08-31", 1, 1, "manifest", IssueType.STRUCTURE, "The mime type should be application/json not "+cnt.getContentType(), IssueSeverity.ERROR); @@ -243,6 +243,7 @@ public class SHLParser extends ParserBase { } private void processContent(List res, List errors, String path, String name, String jose, String ct) throws FHIRFormatError, DefinitionException, FHIRException, IOException { + NamedElement bin = addNamedElement(res, "encrypted", "jose", TextFile.stringToBytes(jose, false)); byte[] cnt = null; JWEObject jwe; try { @@ -250,10 +251,9 @@ public class SHLParser extends ParserBase { jwe.decrypt(new DirectDecrypter(key)); cnt = jwe.getPayload().toBytes(); } catch (Exception e) { - logError(errors, "202-08-31", 1, 1, path, IssueType.STRUCTURE, "Decruption failed: "+e.getMessage(), IssueSeverity.ERROR); + logError(bin.getErrors(), "202-08-31", 1, 1, path, IssueType.STRUCTURE, "Decruption failed: "+e.getMessage(), IssueSeverity.ERROR); } if (cnt != null) { - NamedElement doc = addNamedElement(res, name, cnt); switch (ct) { case "application/smart-health-card": //a JSON file with a .verifiableCredential array containing SMART Health Card JWS strings, as specified by https://spec.smarthealth.cards#via-file-download. @@ -261,23 +261,26 @@ public class SHLParser extends ParserBase { res.addAll(shc.parse(new ByteArrayInputStream(cnt))); break; case "application/fhir+json": + NamedElement doc = addNamedElement(res, name, "json", cnt); // a JSON file containing any FHIR resource (e.g., an individual resource or a Bundle of resources). Generally this format may not be tamper-proof. logError(doc.getErrors(), "202-08-31", 1, 1, name, IssueType.STRUCTURE, "Processing content of type 'application/smart-api-access' is not done yet", IssueSeverity.INFORMATION); break; case "application/smart-api-access": + doc = addNamedElement(res, name, "api.json", cnt); // a JSON file with a SMART Access Token Response (see SMART App Launch). Two additional properties are defined: // aud Required string indicating the FHIR Server Base URL where this token can be used (e.g., "https://server.example.org/fhir") // query: Optional array of strings acting as hints to the client, indicating queries it might want to make (e.g., ["Coverage?patient=123&_tag=family-insurance"]) logError(doc.getErrors(), "202-08-31", 1, 1, name, IssueType.STRUCTURE, "Processing content of type 'application/smart-api-access' is not done yet", IssueSeverity.INFORMATION); break; default: + doc = addNamedElement(res, name, "bin", cnt); logError(doc.getErrors(), "202-08-31", 1, 1, name, IssueType.STRUCTURE, "The Content-Type '"+ct+"' is not known", IssueSeverity.INFORMATION); } } } - private NamedElement addNamedElement(List res, String name, byte[] content) { - NamedElement result = new NamedElement(name, content); + private NamedElement addNamedElement(List res, String name, String type, byte[] content) { + NamedElement result = new NamedElement(name, type, content); res.add(result); return result; } diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/elementmodel/TurtleParser.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/elementmodel/TurtleParser.java index f2b5e731e..35e3c223f 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/elementmodel/TurtleParser.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/elementmodel/TurtleParser.java @@ -83,7 +83,7 @@ public class TurtleParser extends ParserBase { @Override public List parse(InputStream inStream) throws IOException, FHIRException { byte[] content = TextFile.streamToBytes(inStream); - NamedElement ctxt = new NamedElement("ttl", content); + NamedElement ctxt = new NamedElement("focus", "ttl", content); ByteArrayInputStream stream = new ByteArrayInputStream(content); Turtle src = new Turtle(); diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/elementmodel/VerticalBarParser.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/elementmodel/VerticalBarParser.java index 4773cd671..efd1022ab 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/elementmodel/VerticalBarParser.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/elementmodel/VerticalBarParser.java @@ -465,7 +465,7 @@ public class VerticalBarParser extends ParserBase { while (!reader.isFinished()) // && (getOptions().getSegmentLimit() == 0 || getOptions().getSegmentLimit() > message.getSegments().size())) readSegment(message, reader); List res = new ArrayList<>(); - res.add(new NamedElement(null, message, content)); + res.add(new NamedElement("focus", "hl7", message, content)); return res; } diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/elementmodel/XmlParser.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/elementmodel/XmlParser.java index cdb71764d..e077f9256 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/elementmodel/XmlParser.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/elementmodel/XmlParser.java @@ -114,7 +114,7 @@ public class XmlParser extends ParserBase { public List parse(InputStream inStream) throws FHIRFormatError, DefinitionException, FHIRException, IOException { byte[] content = TextFile.streamToBytes(inStream); - NamedElement context = new NamedElement("xml", content); + NamedElement context = new NamedElement("focus", "xml", content); ByteArrayInputStream stream = new ByteArrayInputStream(content); Document doc = null; diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/InstanceValidator.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/InstanceValidator.java index 530dcb8bf..f8426d395 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/InstanceValidator.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/InstanceValidator.java @@ -176,6 +176,7 @@ import org.hl7.fhir.utilities.CommaSeparatedStringBuilder; import org.hl7.fhir.utilities.MarkDownProcessor; import org.hl7.fhir.utilities.SIDUtilities; import org.hl7.fhir.utilities.StandardsStatus; +import org.hl7.fhir.utilities.TextFile; import org.hl7.fhir.utilities.UnicodeUtilities; import org.hl7.fhir.utilities.Utilities; import org.hl7.fhir.utilities.Utilities.DecimalStatus; @@ -269,6 +270,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat private static final String HTML_FRAGMENT_REGEX = "[a-zA-Z]\\w*(((\\s+)(\\S)*)*)"; private static final boolean STACK_TRACE = false; private static final boolean DEBUG_ELEMENT = false; + private static final boolean SAVE_INTERMEDIARIES = false; // set this to true to get the intermediary formats while we are waiting for a UI around this z(SHC/SHL) private static final HashSet NO_TX_SYSTEM_EXEMPT = new HashSet<>(Arrays.asList("http://loinc.org", "http://unitsofmeasure.org", "http://hl7.org/fhir/sid/icd-9-cm", "http://snomed.info/sct", "http://www.nlm.nih.gov/research/umls/rxnorm")); private static final HashSet NO_HTTPS_LIST = new HashSet<>(Arrays.asList("https://loinc.org", "https://unitsofmeasure.org", "https://snomed.info/sct", "https://www.nlm.nih.gov/research/umls/rxnorm")); @@ -741,6 +743,13 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat } timeTracker.load(t); if (validatedContent != null && !validatedContent.isEmpty()) { + if (SAVE_INTERMEDIARIES) { + int index = 0; + for (NamedElement ne : validatedContent) { + index++; + saveValidatedContent(ne, index); + } + } String url = parser.getImpliedProfile(); if (url != null) { StructureDefinition sd = context.fetchResource(StructureDefinition.class, url); @@ -760,6 +769,19 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat return (validatedContent == null || validatedContent.isEmpty()) ? null : validatedContent.get(0).getElement(); // todo: this is broken, but fixing it really complicates things elsewhere, so we do this for now } + private void saveValidatedContent(NamedElement ne, int index) { + String tgt = null; + try { + tgt = Utilities.path("[tmp]", "validator", "content"); + Utilities.createDirectory(tgt); + tgt = Utilities.path(tgt, "content-"+index+"-"+ne.getFilename()); + TextFile.bytesToFile(ne.getContent(), tgt); + } catch (Exception e) { + System.out.println("Error saving internal content to '"+tgt+"': "+e.getLocalizedMessage()); + } + + } + @Override public org.hl7.fhir.r5.elementmodel.Element validate(Object appContext, List errors, Resource resource) throws FHIRException { return validate(appContext, errors, resource, new ArrayList<>()); @@ -4986,6 +5008,30 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat pct.done(); } + + if (defn.hasExtension(ToolingExtensions.EXT_SD_IMPOSE_PROFILE)) { + for (Extension ext : defn.getExtensionsByUrl(ToolingExtensions.EXT_SD_IMPOSE_PROFILE)) { + StructureDefinition sdi = context.fetchResource(StructureDefinition.class, ext.getValue().primitiveValue()); + if (sdi == null) { + warning(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, element.line(), element.col(), stack.getLiteralPath(), false, I18nConstants.VALIDATION_VAL_PROFILE_DEPENDS_NOT_RESOLVED, ext.getValue().primitiveValue(), defn.getVersionedUrl()); + } else { + if (crumbTrails) { + element.addMessage(signpost(errors, NO_RULE_DATE, IssueType.INFORMATIONAL, element.line(), element.col(), stack.getLiteralPath(), I18nConstants.VALIDATION_VAL_PROFILE_SIGNPOST_DEP, sdi.getUrl(), defn.getVersionedUrl())); + } + stack.resetIds(); + if (pctOwned) { + pct = new PercentageTracker(resource.countDescendents(), resource.fhirType(), sdi.getUrl(), logProgress); + } + ok = startInner(hostContext, errors, resource, element, sdi, stack, false, pct, mode.withSource(ProfileSource.ProfileDependency)) && ok; + if (pctOwned) { + pct.done(); + } + + } + } + } + + Element meta = element.getNamedChild(META); if (meta != null) { List profiles = new ArrayList();