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 f2a3418e0..d4b633e02 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 @@ -36,6 +36,7 @@ import java.io.InputStream; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.math.BigDecimal; +import java.util.ArrayList; import java.util.HashSet; import java.util.IdentityHashMap; import java.util.List; @@ -108,8 +109,9 @@ public class JsonParser extends ParserBase { @Override - public Element parse(InputStream stream) throws IOException, FHIRException { + public List parse(InputStream stream) throws IOException, FHIRException { // if we're parsing at this point, then we're going to use the custom parser + List res = new ArrayList<>(); map = new IdentityHashMap(); String source = TextFile.streamToString(stream); if (policy == ValidationPolicy.EVERYTHING) { @@ -121,12 +123,19 @@ public class JsonParser extends ParserBase { return null; } assert (map.containsKey(obj)); - return parse(obj); + Element e = parse(obj); + if (e != null) { + res.add(new NamedElement(null, e)); + } } else { JsonObject obj = JsonTrackingParser.parse(source, null); // (JsonObject) new com.google.gson.JsonParser().parse(source); // assert (map.containsKey(obj)); - return parse(obj); + Element e = parse(obj); + if (e != null) { + res.add(new NamedElement(null, e)); + } } + return res; } public Element parse(JsonObject object, Map map) throws FHIRException { diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/elementmodel/Manager.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/elementmodel/Manager.java index 658789419..be9918f74 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/elementmodel/Manager.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/elementmodel/Manager.java @@ -34,18 +34,21 @@ package org.hl7.fhir.r5.elementmodel; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.util.List; import org.hl7.fhir.exceptions.DefinitionException; import org.hl7.fhir.exceptions.FHIRException; import org.hl7.fhir.exceptions.FHIRFormatError; import org.hl7.fhir.r5.context.IWorkerContext; +import org.hl7.fhir.r5.elementmodel.ParserBase.NamedElement; import org.hl7.fhir.r5.formats.IParser.OutputStyle; import org.hl7.fhir.r5.model.StructureDefinition; public class Manager { //TODO use EnumMap - public enum FhirFormat { XML, JSON, TURTLE, TEXT, VBAR; + public enum FhirFormat { XML, JSON, TURTLE, TEXT, VBAR, SHC; + // SHC = smart health cards, including as text versions of QR codes public String getExtension() { switch (this) { @@ -82,10 +85,15 @@ public class Manager { } - public static Element parse(IWorkerContext context, InputStream source, FhirFormat inputFormat) throws FHIRFormatError, DefinitionException, IOException, FHIRException { + public static List parse(IWorkerContext context, InputStream source, FhirFormat inputFormat) throws FHIRFormatError, DefinitionException, IOException, FHIRException { return makeParser(context, inputFormat).parse(source); } + public static Element parseSingle(IWorkerContext context, InputStream source, FhirFormat inputFormat) throws FHIRFormatError, DefinitionException, IOException, FHIRException { + return makeParser(context, inputFormat).parseSingle(source); + } + + public static void compose(IWorkerContext context, Element e, OutputStream destination, FhirFormat outputFormat, OutputStyle style, String base) throws FHIRException, IOException { makeParser(context, outputFormat).compose(e, destination, style, base); } @@ -96,6 +104,7 @@ public class Manager { case XML : return new XmlParser(context); case TURTLE : return new TurtleParser(context); case VBAR : return new VerticalBarParser(context); + case SHC : return new SHCParser(context); case TEXT : throw new Error("Programming logic error: do not call makeParser for a text resource"); } return null; diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/elementmodel/ObjectConverter.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/elementmodel/ObjectConverter.java index 905961585..719f54236 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/elementmodel/ObjectConverter.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/elementmodel/ObjectConverter.java @@ -38,6 +38,7 @@ import java.util.List; import org.hl7.fhir.exceptions.FHIRException; import org.hl7.fhir.r5.conformance.ProfileUtilities; import org.hl7.fhir.r5.context.IWorkerContext; +import org.hl7.fhir.r5.elementmodel.ParserBase.NamedElement; import org.hl7.fhir.r5.formats.IParser.OutputStyle; import org.hl7.fhir.r5.model.Base; import org.hl7.fhir.r5.model.CodeableConcept; @@ -70,7 +71,11 @@ public class ObjectConverter { org.hl7.fhir.r5.formats.JsonParser jp = new org.hl7.fhir.r5.formats.JsonParser(); jp.compose(bs, ig); ByteArrayInputStream bi = new ByteArrayInputStream(bs.toByteArray()); - return new JsonParser(context).parse(bi); + List list = new JsonParser(context).parse(bi); + if (list.size() != 1) { + throw new FHIRException("Unable to convert because the source contains multieple resources"); + } + return list.get(0).getElement(); } public Element convert(Property property, DataType type) throws FHIRException { 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 46497484d..c3d2a3023 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 @@ -40,6 +40,7 @@ import org.hl7.fhir.exceptions.DefinitionException; import org.hl7.fhir.exceptions.FHIRException; import org.hl7.fhir.exceptions.FHIRFormatError; import org.hl7.fhir.r5.context.IWorkerContext; +import org.hl7.fhir.r5.elementmodel.ParserBase.NamedElement; import org.hl7.fhir.r5.formats.FormatUtilities; import org.hl7.fhir.r5.formats.IParser.OutputStyle; import org.hl7.fhir.r5.model.StructureDefinition; @@ -54,6 +55,23 @@ import org.hl7.fhir.utilities.validation.ValidationMessage.Source; public abstract class ParserBase { + public class NamedElement { + private String name; + private Element element; + public NamedElement(String name, Element element) { + super(); + this.name = name; + this.element = element; + } + public String getName() { + return name; + } + public Element getElement() { + return element; + } + + } + public interface ILinkResolver { String resolveType(String type); String resolveProperty(Property property); @@ -86,7 +104,15 @@ public abstract class ParserBase { this.errors = errors; } - public abstract Element parse(InputStream stream) throws IOException, FHIRFormatError, DefinitionException, FHIRException; + public abstract List parse(InputStream stream) throws IOException, FHIRFormatError, DefinitionException, FHIRException; + + public Element parseSingle(InputStream stream) throws IOException, FHIRFormatError, DefinitionException, FHIRException { + List res = parse(stream); + if (res.size() != 1) { + throw new FHIRException("Parsing FHIR content returned multiple elements in a context where only one element is allowed"); + } + return res.get(0).getElement(); + } public abstract void compose(Element e, OutputStream destination, OutputStyle style, String base) throws FHIRException, IOException; @@ -161,5 +187,9 @@ public abstract class ParserBase { this.showDecorations = showDecorations; } + public String getImpliedProfile() { + return null; + } + } \ No newline at end of file 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 new file mode 100644 index 000000000..6d4d61709 --- /dev/null +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/elementmodel/SHCParser.java @@ -0,0 +1,309 @@ +package org.hl7.fhir.r5.elementmodel; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Base64; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.zip.DataFormatException; +import java.util.zip.Inflater; + +import org.apache.commons.io.IOUtils; +import org.hl7.fhir.exceptions.DefinitionException; +import org.hl7.fhir.exceptions.FHIRException; +import org.hl7.fhir.exceptions.FHIRFormatError; +import org.hl7.fhir.r5.context.IWorkerContext; +import org.hl7.fhir.r5.formats.IParser.OutputStyle; +import org.hl7.fhir.utilities.TextFile; +import org.hl7.fhir.utilities.Utilities; +import org.hl7.fhir.utilities.VersionUtilities; +import org.hl7.fhir.utilities.json.JSONUtil; +import org.hl7.fhir.utilities.json.JsonTrackingParser; +import org.hl7.fhir.utilities.json.JsonTrackingParser.LocationData; +import org.hl7.fhir.utilities.validation.ValidationMessage.IssueSeverity; +import org.hl7.fhir.utilities.validation.ValidationMessage.IssueType; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonNull; +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; + +/** + * this class is actually a smart health cards validator. + * It's going to parse the JWT and assume that it contains + * a smart health card, which has a nested bundle in it, and + * then validate the bundle. + * + * See https://spec.smarthealth.cards/#health-cards-are-encoded-as-compact-serialization-json-web-signatures-jws + * + * This parser dose the JWT work, and then passes the JsonObject through to the underlying JsonParser + * + * Error locations are in the decoded payload + * + * @author grahame + * + */ +public class SHCParser extends ParserBase { + + private JsonParser jsonParser; + private Map map; + private List types = new ArrayList<>(); + + public SHCParser(IWorkerContext context) { + super(context); + jsonParser = new JsonParser(context); + } + + public List parse(InputStream stream) throws IOException, FHIRFormatError, DefinitionException, FHIRException { + List res = new ArrayList<>(); + String src = TextFile.streamToString(stream).trim(); + List list = new ArrayList<>(); + String pfx = null; + if (src.startsWith("{")) { + JsonObject json = JsonTrackingParser.parseJson(src); + if (checkProperty(json, "$", "verifiableCredential", true, "Array")) { + pfx = "verifiableCredential"; + JsonArray arr = json.getAsJsonArray("verifiableCredential"); + int i = 0; + for (JsonElement e : arr) { + if (!(e instanceof JsonPrimitive)) { + logError(line(e), col(e), "$.verifiableCredential["+i+"]", IssueType.STRUCTURE, "Wrong Property verifiableCredential in JSON Payload. Expected : String but found "+JSONUtil.type(e), IssueSeverity.ERROR); + } else { + list.add(e.getAsString()); + } + i++; + } + } else { + return res; + } + } else { + list.add(src); + } + int c = 0; + for (String ssrc : list) { + String prefix = pfx == null ? "" : pfx+"["+Integer.toString(c)+"]."; + c++; + JWT jwt = null; + try { + jwt = decodeJWT(ssrc); + } catch (Exception e) { + logError(1, 1, prefix+"JWT", IssueType.INVALID, "Unable to decode JWT token", IssueSeverity.ERROR); + return res; + } + map = jwt.map; + JsonTrackingParser.write(jwt.payload, "c:\\temp\\payload.json"); + logError(1, 1, prefix+"JWT", IssueType.INFORMATIONAL, "The FHIR Validator does not check the JWT signature (see https://demo-portals.smarthealth.cards/VerifierPortal.html or https://github.com/smart-on-fhir/health-cards-dev-tools)", IssueSeverity.INFORMATION); + checkNamedProperties(jwt.getPayload(), prefix+"payload", "iss", "nbf", "vc"); + checkProperty(jwt.getPayload(), prefix+"payload", "iss", true, "String"); + checkProperty(jwt.getPayload(), prefix+"payload", "nbf", true, "Number"); + JsonObject vc = jwt.getPayload().getAsJsonObject("vc"); + if (vc == null) { + logError(1, 1, "JWT", IssueType.STRUCTURE, "Unable to find property 'vc' in the payload", IssueSeverity.ERROR); + return res; + } + String path = prefix+"payload.vc"; + checkNamedProperties(vc, path, "type", "credentialSubject"); + if (!checkProperty(vc, path, "type", true, "Array")) { + return res; + } + JsonArray type = vc.getAsJsonArray("type"); + int i = 0; + for (JsonElement e : type) { + if (!(e instanceof JsonPrimitive)) { + logError(line(e), col(e), path+".type["+i+"]", IssueType.STRUCTURE, "Wrong Property Type in JSON Payload. Expected : String but found "+JSONUtil.type(e), IssueSeverity.ERROR); + } else { + types.add(e.getAsString()); + } + i++; + } + if (!types.contains("https://smarthealth.cards#health-card")) { + logError(line(vc), col(vc), path, IssueType.STRUCTURE, "Card does not claim to be of type https://smarthealth.cards#health-card, cannot validate", IssueSeverity.ERROR); + return res; + } + if (!checkProperty(vc, path, "credentialSubject", true, "Object")) { + return res; + } + JsonObject cs = vc.getAsJsonObject("credentialSubject"); + path = path+".credentialSubject"; + if (!checkProperty(cs, path, "fhirVersion", true, "String")) { + return res; + } + JsonElement fv = cs.get("fhirVersion"); + if (!VersionUtilities.versionsCompatible(context.getVersion(), fv.getAsString())) { + logError(line(fv), col(fv), path+".fhirVersion", IssueType.STRUCTURE, "Card claims to be of version "+fv.getAsString()+", cannot be validated against version "+context.getVersion(), IssueSeverity.ERROR); + return res; + } + if (!checkProperty(cs, path, "fhirBundle", true, "Object")) { + return res; + } + // ok. all checks passed, we can now validate the bundle + Element e = jsonParser.parse(cs.getAsJsonObject("fhirBundle"), map); + if (e != null) { + res.add(new NamedElement(path, e)); + } + } + return res; + } + + + @Override + public String getImpliedProfile() { + if (types.contains("https://smarthealth.cards#covid19") && types.contains("https://smarthealth.cards#immunization")) { + return "http://hl7.org/fhir/uv/shc-vaccination/StructureDefinition/shc-vaccination-bundle-dm"; + } + if (types.contains("https://smarthealth.cards#covid19") && types.contains("https://smarthealth.cards#laboratory")) { + return "http://hl7.org/fhir/uv/shc-vaccination/StructureDefinition/shc-covid19-laboratory-bundle-dm"; + } + if (types.contains("https://smarthealth.cards#laboratory")) { + return "http://hl7.org/fhir/uv/shc-vaccination/StructureDefinition/shc-infectious-disease-laboratory-bundle-dm"; + } + return null; + } + + + private boolean checkProperty(JsonObject obj, String path, String name, boolean required, String type) { + JsonElement e = obj.get(name); + if (e != null) { + String t = JSONUtil.type(e); + if (!type.equals(t)) { + logError(line(e), col(e), path+"."+name, IssueType.STRUCTURE, "Wrong Property Type in JSON Payload. Expected : "+type+" but found "+t, IssueSeverity.ERROR); + } else { + return true; + } + } else if (required) { + logError(line(obj), col(obj), path, IssueType.STRUCTURE, "Missing Property in JSON Payload: "+name, IssueSeverity.ERROR); + } else { + return true; + } + return false; + } + + private void checkNamedProperties(JsonObject obj, String path, String... names) { + for (Entry e : obj.entrySet()) { + if (!Utilities.existsInList(e.getKey(), names)) { + logError(line(e.getValue()), col(e.getValue()), path+"."+e.getKey(), IssueType.STRUCTURE, "Unknown Property in JSON Payload", IssueSeverity.WARNING); + } + } + } + + private int line(JsonElement e) { + if (map == null|| !map.containsKey(e)) + return -1; + else + return map.get(e).getLine(); + } + + private int col(JsonElement e) { + if (map == null|| !map.containsKey(e)) + return -1; + else + return map.get(e).getCol(); + } + + + + public void compose(Element e, OutputStream destination, OutputStyle style, String base) throws FHIRException, IOException { + throw new FHIRFormatError("Writing resources is not supported for the SHC format"); + // because then we'd have to try to sign, and we're just not going to be doing that from the element model + } + + + public static class JWT { + + private JsonObject header; + private JsonObject payload; + public Map map = new HashMap<>(); + + public JsonObject getHeader() { + return header; + } + public void setHeader(JsonObject header) { + this.header = header; + } + public JsonObject getPayload() { + return payload; + } + public void setPayload(JsonObject payload) { + this.payload = payload; + } + } + + private static final int BUFFER_SIZE = 1024; + public static final String CURRENT_PACKAGE = "hl7.fhir.uv.shc-vaccination#0.6.2"; + + // todo: deal with chunking + public static String decodeQRCode(String src) { + StringBuilder b = new StringBuilder(); + if (!src.startsWith("shc:/")) { + throw new FHIRException("Unable to process smart health card (didn't start with shc:/)"); + } + for (int i = 5; i < src.length(); i = i + 2) { + String s = src.substring(i, i+2); + byte v = Byte.parseByte(s); + char c = (char) (45+v); + b.append(c); + } + return b.toString(); + } + + public static JWT decodeJWT(String jwt) throws IOException, DataFormatException { + if (jwt.startsWith("shc:/")) { + jwt = decodeQRCode(jwt); + } + String[] parts = splitToken(jwt); + byte[] headerJson; + byte[] payloadJson; + try { + headerJson = Base64.getUrlDecoder().decode(parts[0]); + payloadJson = Base64.getUrlDecoder().decode(parts[1]); + } catch (NullPointerException e) { + throw new FHIRException("The UTF-8 Charset isn't initialized.", e); + } catch (IllegalArgumentException e){ + throw new FHIRException("The input is not a valid base 64 encoded string.", e); + } + JWT res = new JWT(); + res.header = JsonTrackingParser.parseJson(headerJson); + if ("DEF".equals(JSONUtil.str(res.header, "zip"))) { + payloadJson = inflate(payloadJson); + } + res.payload = JsonTrackingParser.parse(TextFile.bytesToString(payloadJson), res.map, true); + return res; + } + + static String[] splitToken(String token) { + String[] parts = token.split("\\."); + if (parts.length == 2 && token.endsWith(".")) { + //Tokens with alg='none' have empty String as Signature. + parts = new String[]{parts[0], parts[1], ""}; + } + if (parts.length != 3) { + throw new FHIRException(String.format("The token was expected to have 3 parts, but got %s.", parts.length)); + } + return parts; + } + + public static final byte[] inflate(byte[] data) throws IOException, DataFormatException { + final Inflater inflater = new Inflater(true); + inflater.setInput(data); + + try (final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(data.length)) + { + byte[] buffer = new byte[BUFFER_SIZE]; + while (!inflater.finished()) + { + final int count = inflater.inflate(buffer); + outputStream.write(buffer, 0, count); + } + + return outputStream.toByteArray(); + } + } + + +} diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/elementmodel/Tester.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/elementmodel/Tester.java index 52e68067a..03248a326 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/elementmodel/Tester.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/elementmodel/Tester.java @@ -34,11 +34,13 @@ package org.hl7.fhir.r5.elementmodel; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; +import java.util.List; import java.util.Map.Entry; import org.hl7.fhir.r5.context.IWorkerContext; import org.hl7.fhir.r5.context.SimpleWorkerContext; import org.hl7.fhir.r5.elementmodel.Manager.FhirFormat; +import org.hl7.fhir.r5.elementmodel.ParserBase.NamedElement; import org.hl7.fhir.r5.formats.IParser.OutputStyle; import org.hl7.fhir.utilities.TextFile; import org.hl7.fhir.utilities.Utilities; @@ -61,7 +63,7 @@ public class Tester { // new FileOutputStream("C:\\work\\org.hl7.fhir\\build\\publish\\"+Utilities.changeFileExt(f, ".mm.json")), FhirFormat.JSON, OutputStyle.PRETTY); // String src = normalise(TextFile.fileToString("C:\\work\\org.hl7.fhir\\build\\publish\\"+Utilities.changeFileExt(f, ".mm.json"))); // String tgt = normalise(TextFile.fileToString("C:\\work\\org.hl7.fhir\\build\\publish\\"+Utilities.changeFileExt(f, ".json"))); - Element e = Manager.parse(context, new FileInputStream("C:\\work\\org.hl7.fhir\\build\\publish\\"+f), FhirFormat.XML); + Element e = Manager.parseSingle(context, new FileInputStream("C:\\work\\org.hl7.fhir\\build\\publish\\"+f), FhirFormat.XML); Manager.compose(context, e, new FileOutputStream("C:\\work\\org.hl7.fhir\\build\\publish\\"+Utilities.changeFileExt(f, ".mm.ttl")), FhirFormat.TURTLE, OutputStyle.PRETTY, null); Manager.compose(context, e, new FileOutputStream("C:\\temp\\resource.xml"), FhirFormat.XML, OutputStyle.PRETTY, null); String src = TextFile.fileToString("C:\\work\\org.hl7.fhir\\build\\publish\\"+Utilities.changeFileExt(f, ".mm.ttl")); 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 5a3e509b5..f5bca0176 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 @@ -34,6 +34,7 @@ package org.hl7.fhir.r5.elementmodel; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -42,6 +43,7 @@ import org.hl7.fhir.exceptions.FHIRException; import org.hl7.fhir.exceptions.FHIRFormatError; import org.hl7.fhir.r5.context.IWorkerContext; import org.hl7.fhir.r5.elementmodel.Element.SpecialElement; +import org.hl7.fhir.r5.elementmodel.ParserBase.NamedElement; import org.hl7.fhir.r5.formats.IParser.OutputStyle; import org.hl7.fhir.r5.model.ElementDefinition.TypeRefComponent; import org.hl7.fhir.r5.model.StructureDefinition; @@ -74,7 +76,8 @@ public class TurtleParser extends ParserBase { super(context); } @Override - public Element parse(InputStream input) throws IOException, FHIRException { + public List parse(InputStream input) throws IOException, FHIRException { + List res = new ArrayList<>(); Turtle src = new Turtle(); if (policy == ValidationPolicy.EVERYTHING) { try { @@ -83,11 +86,18 @@ public class TurtleParser extends ParserBase { logError(-1, -1, "(document)", IssueType.INVALID, context.formatMessage(I18nConstants.ERROR_PARSING_TURTLE_, e.getMessage()), IssueSeverity.FATAL); return null; } - return parse(src); + Element e = parse(src); + if (e != null) { + res.add(new NamedElement(null, e)); + } } else { - src.parse(TextFile.streamToString(input)); - return parse(src); - } + src.parse(TextFile.streamToString(input)); + Element e = parse(src); + if (e != null) { + res.add(new NamedElement(null, e)); + } + } + return res; } private Element parse(Turtle src) throws FHIRException { 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 e070b5231..27a8e3f33 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 @@ -36,11 +36,14 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; +import java.util.ArrayList; +import java.util.List; import org.hl7.fhir.exceptions.DefinitionException; import org.hl7.fhir.exceptions.FHIRException; import org.hl7.fhir.exceptions.FHIRFormatError; import org.hl7.fhir.r5.context.IWorkerContext; +import org.hl7.fhir.r5.elementmodel.ParserBase.NamedElement; import org.hl7.fhir.r5.formats.IParser.OutputStyle; import org.hl7.fhir.r5.model.StructureDefinition; @@ -450,7 +453,7 @@ public class VerticalBarParser extends ParserBase { private Delimiters delimiters = new Delimiters(); @Override - public Element parse(InputStream stream) throws IOException, FHIRFormatError, DefinitionException, FHIRException { + public List parse(InputStream stream) throws IOException, FHIRFormatError, DefinitionException, FHIRException { StructureDefinition sd = context.fetchResource(StructureDefinition.class, "http://hl7.org/fhir/v2/StructureDefinition/Message"); Element message = new Element("Message", new Property(context, sd.getSnapshot().getElementFirstRep(), sd)); VerticalBarParserReader reader = new VerticalBarParserReader(new BufferedInputStream(stream), charset); @@ -458,8 +461,9 @@ public class VerticalBarParser extends ParserBase { preDecode(reader); while (!reader.isFinished()) // && (getOptions().getSegmentLimit() == 0 || getOptions().getSegmentLimit() > message.getSegments().size())) readSegment(message, reader); - - return message; + List res = new ArrayList<>(); + res.add(new NamedElement(null, message)); + return res; } private void preDecode(VerticalBarParserReader reader) throws FHIRException { 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 88f38a5f5..892080497 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 @@ -53,6 +53,7 @@ import org.hl7.fhir.exceptions.FHIRFormatError; import org.hl7.fhir.r5.conformance.ProfileUtilities; import org.hl7.fhir.r5.context.IWorkerContext; import org.hl7.fhir.r5.elementmodel.Element.SpecialElement; +import org.hl7.fhir.r5.elementmodel.ParserBase.NamedElement; import org.hl7.fhir.r5.formats.FormatUtilities; import org.hl7.fhir.r5.formats.IParser.OutputStyle; import org.hl7.fhir.r5.model.DateTimeType; @@ -106,7 +107,8 @@ public class XmlParser extends ParserBase { this.allowXsiLocation = allowXsiLocation; } - public Element parse(InputStream stream) throws FHIRFormatError, DefinitionException, FHIRException, IOException { + public List parse(InputStream stream) throws FHIRFormatError, DefinitionException, FHIRException, IOException { + List res = new ArrayList<>(); Document doc = null; try { DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); @@ -157,10 +159,13 @@ public class XmlParser extends ParserBase { logError(0, 0, "(syntax)", IssueType.INVALID, e.getMessage(), IssueSeverity.FATAL); doc = null; } - if (doc == null) - return null; - else - return parse(doc); + if (doc != null) { + Element e = parse(doc); + if (e != null) { + res.add(new NamedElement(null, e)); + } + } + return res; } diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/IResourceValidator.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/IResourceValidator.java index f5ab32be8..264a83454 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/IResourceValidator.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/IResourceValidator.java @@ -281,9 +281,9 @@ public interface IResourceValidator { * in addition, you can pass one or more profiles ti validate beyond the base standard - as structure definitions or canonical URLs * @throws IOException */ - void validate(Object Context, List errors, org.hl7.fhir.r5.elementmodel.Element element) throws FHIRException; - void validate(Object Context, List errors, org.hl7.fhir.r5.elementmodel.Element element, String profile) throws FHIRException; - void validate(Object Context, List errors, org.hl7.fhir.r5.elementmodel.Element element, List profiles) throws FHIRException; + void validate(Object Context, List errors, String initialPath, org.hl7.fhir.r5.elementmodel.Element element) throws FHIRException; + void validate(Object Context, List errors, String initialPath, org.hl7.fhir.r5.elementmodel.Element element, String profile) throws FHIRException; + void validate(Object Context, List errors, String initialPath, org.hl7.fhir.r5.elementmodel.Element element, List profiles) throws FHIRException; org.hl7.fhir.r5.elementmodel.Element validate(Object Context, List errors, InputStream stream, FhirFormat format) throws FHIRException; org.hl7.fhir.r5.elementmodel.Element validate(Object Context, List errors, InputStream stream, FhirFormat format, String profile) throws FHIRException; diff --git a/org.hl7.fhir.r5/src/test/java/org/hl7/fhir/r5/test/CDARoundTripTests.java b/org.hl7.fhir.r5/src/test/java/org/hl7/fhir/r5/test/CDARoundTripTests.java index 13408d869..b39dc90ec 100644 --- a/org.hl7.fhir.r5/src/test/java/org/hl7/fhir/r5/test/CDARoundTripTests.java +++ b/org.hl7.fhir.r5/src/test/java/org/hl7/fhir/r5/test/CDARoundTripTests.java @@ -207,14 +207,14 @@ public class CDARoundTripTests { * @throws IOException */ public void testClinicalDocumentXmlParser() throws IOException { - Element cda = Manager.parse(context, TestingUtilities.loadTestResourceStream("validator", "cda", "example.xml"), + Element cda = Manager.parseSingle(context, TestingUtilities.loadTestResourceStream("validator", "cda", "example.xml"), FhirFormat.XML); assertsExample(cda); ByteArrayOutputStream baosXml = new ByteArrayOutputStream(); Manager.compose(context, cda, baosXml, FhirFormat.XML, OutputStyle.PRETTY, null); - Element cdaXmlRoundtrip = Manager.parse(context, new ByteArrayInputStream(baosXml.toString().getBytes()), FhirFormat.XML); + Element cdaXmlRoundtrip = Manager.parseSingle(context, new ByteArrayInputStream(baosXml.toString().getBytes()), FhirFormat.XML); assertsExample(cdaXmlRoundtrip); } @@ -226,14 +226,14 @@ public class CDARoundTripTests { * @throws IOException */ public void testClinicalDocumentJsonParser() throws IOException { - Element cda = Manager.parse(context, TestingUtilities.loadTestResourceStream("validator", "cda", "example.xml"), + Element cda = Manager.parseSingle(context, TestingUtilities.loadTestResourceStream("validator", "cda", "example.xml"), FhirFormat.XML); assertsExample(cda); ByteArrayOutputStream baosJson = new ByteArrayOutputStream(); Manager.compose(context, cda, baosJson, FhirFormat.JSON, OutputStyle.PRETTY, null); - Element cdaJsonRoundtrip = Manager.parse(context, new ByteArrayInputStream(baosJson.toString().getBytes()), + Element cdaJsonRoundtrip = Manager.parseSingle(context, new ByteArrayInputStream(baosJson.toString().getBytes()), FhirFormat.JSON); assertsExample(cdaJsonRoundtrip); @@ -245,7 +245,7 @@ public class CDARoundTripTests { * verify that umlaut like äö etc are not encoded in UTF-8 in attributes */ public void testSerializeUmlaut() throws IOException { - Element xml = Manager.parse(context, + Element xml = Manager.parseSingle(context, TestingUtilities.loadTestResourceStream("validator", "cda", "example.xml"), FhirFormat.XML); List title = xml.getChildrenByName("title"); diff --git a/org.hl7.fhir.r5/src/test/java/org/hl7/fhir/r5/test/NarrativeGenerationTests.java b/org.hl7.fhir.r5/src/test/java/org/hl7/fhir/r5/test/NarrativeGenerationTests.java index 6bb55c3ff..7c5b733dc 100644 --- a/org.hl7.fhir.r5/src/test/java/org/hl7/fhir/r5/test/NarrativeGenerationTests.java +++ b/org.hl7.fhir.r5/src/test/java/org/hl7/fhir/r5/test/NarrativeGenerationTests.java @@ -148,7 +148,7 @@ public class NarrativeGenerationTests { Assertions.assertTrue(output.equals(target), "Output does not match expected"); if (test.isMeta()) { - org.hl7.fhir.r5.elementmodel.Element e = Manager.parse(context, TestingUtilities.loadTestResourceStream("r5", "narrative", test.getId() + ".xml"), FhirFormat.XML); + org.hl7.fhir.r5.elementmodel.Element e = Manager.parseSingle(context, TestingUtilities.loadTestResourceStream("r5", "narrative", test.getId() + ".xml"), FhirFormat.XML); x = RendererFactory.factory(source, rc).render(new ElementWrappers.ResourceWrapperMetaElement(rc, e)); target = TextFile.streamToString(TestingUtilities.loadTestResourceStream("r5", "narrative", test.getId() + "-meta.html")); diff --git a/org.hl7.fhir.r5/src/test/java/org/hl7/fhir/r5/test/ResourceRoundTripTests.java b/org.hl7.fhir.r5/src/test/java/org/hl7/fhir/r5/test/ResourceRoundTripTests.java index 4d4bcf27e..f9c5cb06e 100644 --- a/org.hl7.fhir.r5/src/test/java/org/hl7/fhir/r5/test/ResourceRoundTripTests.java +++ b/org.hl7.fhir.r5/src/test/java/org/hl7/fhir/r5/test/ResourceRoundTripTests.java @@ -66,7 +66,7 @@ public class ResourceRoundTripTests { * verify that umlaut like äö etc are not encoded in UTF-8 in attributes */ public void testSerializeUmlaut() throws IOException { - Element xml = Manager.parse(TestingUtilities.context(), TestingUtilities.loadTestResourceStream("r5", "unicode.xml"), + Element xml = Manager.parseSingle(TestingUtilities.context(), TestingUtilities.loadTestResourceStream("r5", "unicode.xml"), FhirFormat.XML); List concept = xml.getChildrenByName("concept"); assertTrue(concept!=null && concept.size()==1); diff --git a/org.hl7.fhir.r5/src/test/java/org/hl7/fhir/r5/test/ValidationTestConvertor.java b/org.hl7.fhir.r5/src/test/java/org/hl7/fhir/r5/test/ValidationTestConvertor.java index 6380a1c88..41db20732 100644 --- a/org.hl7.fhir.r5/src/test/java/org/hl7/fhir/r5/test/ValidationTestConvertor.java +++ b/org.hl7.fhir.r5/src/test/java/org/hl7/fhir/r5/test/ValidationTestConvertor.java @@ -30,7 +30,7 @@ public class ValidationTestConvertor { if (!t.exists()) { try { System.out.print("Process " + f.getAbsolutePath()); - Element e = Manager.parse(context, new FileInputStream(f), FhirFormat.XML); + Element e = Manager.parseSingle(context, new FileInputStream(f), FhirFormat.XML); Manager.compose(context, e, new FileOutputStream(t), FhirFormat.TURTLE, OutputStyle.PRETTY, null); System.out.println(" .... success"); } catch (Exception e) { @@ -44,7 +44,7 @@ public class ValidationTestConvertor { if (!t.exists()) { try { System.out.print("Process " + f.getAbsolutePath()); - Element e = Manager.parse(context, new FileInputStream(f), FhirFormat.JSON); + Element e = Manager.parseSingle(context, new FileInputStream(f), FhirFormat.JSON); Manager.compose(context, e, new FileOutputStream(t), FhirFormat.TURTLE, OutputStyle.PRETTY, null); System.out.println(" .... success"); } catch (Exception e) { diff --git a/org.hl7.fhir.r5/src/test/java/org/hl7/fhir/r5/test/XmlParserTests.java b/org.hl7.fhir.r5/src/test/java/org/hl7/fhir/r5/test/XmlParserTests.java index c7f61adda..4d9b6d97f 100644 --- a/org.hl7.fhir.r5/src/test/java/org/hl7/fhir/r5/test/XmlParserTests.java +++ b/org.hl7.fhir.r5/src/test/java/org/hl7/fhir/r5/test/XmlParserTests.java @@ -51,7 +51,7 @@ public class XmlParserTests { * @throws IOException */ public void testXsiDeserialiserXmlParser() throws IOException { - Element cda = Manager.parse(context, TestingUtilities.loadTestResourceStream("validator", "cda", "example-xsi.xml"), + Element cda = Manager.parseSingle(context, TestingUtilities.loadTestResourceStream("validator", "cda", "example-xsi.xml"), FhirFormat.XML); ByteArrayOutputStream baosXml = new ByteArrayOutputStream(); diff --git a/org.hl7.fhir.r5/src/test/java/org/hl7/fhir/r5/test/misc/ResourceTest.java b/org.hl7.fhir.r5/src/test/java/org/hl7/fhir/r5/test/misc/ResourceTest.java index c7302a300..f4233fcb2 100644 --- a/org.hl7.fhir.r5/src/test/java/org/hl7/fhir/r5/test/misc/ResourceTest.java +++ b/org.hl7.fhir.r5/src/test/java/org/hl7/fhir/r5/test/misc/ResourceTest.java @@ -86,7 +86,7 @@ public class ResourceTest { } public Element testEM() throws Exception { - Element resource = Manager.parse(TestingUtilities.context(), new FileInputStream(source), isJson() ? FhirFormat.JSON : FhirFormat.XML); + Element resource = Manager.parseSingle(TestingUtilities.context(), new FileInputStream(source), isJson() ? FhirFormat.JSON : FhirFormat.XML); Manager.compose(TestingUtilities.context(), resource, new FileOutputStream(source.getAbsoluteFile()+".out.json"), FhirFormat.JSON, OutputStyle.PRETTY, null); Manager.compose(TestingUtilities.context(), resource, new FileOutputStream(source.getAbsoluteFile()+".out.json"), FhirFormat.XML, OutputStyle.PRETTY, null); return resource; diff --git a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/json/JSONUtil.java b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/json/JSONUtil.java index 2882dabf5..c2ca45f2d 100644 --- a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/json/JSONUtil.java +++ b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/json/JSONUtil.java @@ -45,6 +45,7 @@ import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonNull; import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; public class JSONUtil { @@ -139,4 +140,27 @@ public class JSONUtil { return (JsonObject) new com.google.gson.JsonParser().parse(TextFile.streamToString(c.getInputStream())); } + public static String type(JsonElement e) { + if (e == null) { + return "(null)"; + } + if (e.isJsonObject()) { + return "Object"; + } + if (e.isJsonArray()) { + return "Array"; + } + if (e.isJsonNull()) { + return "Null"; + } + JsonPrimitive p = (JsonPrimitive) e; + if (p.isBoolean()) { + return "Boolean"; + } + if (p.isNumber()) { + return "Number"; + } + return "String"; + } + } \ No newline at end of file diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/IgLoader.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/IgLoader.java index f918c648e..bcf4283f1 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/IgLoader.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/IgLoader.java @@ -189,7 +189,7 @@ public class IgLoader { return readZip(new FileInputStream(src)); if (src.endsWith("igpack.zip")) return readZip(new FileInputStream(src)); - Manager.FhirFormat fmt = ResourceChecker.checkIsResource(getContext(), isDebug(), src); + Manager.FhirFormat fmt = ResourceChecker.checkIsResource(getContext(), isDebug(), TextFile.fileToBytes(f), src, true); if (fmt != null) { Map res = new HashMap(); res.put(Utilities.changeFileExt(src, "." + fmt.getExtension()), TextFile.fileToBytesNCS(src)); @@ -321,7 +321,7 @@ public class IgLoader { return readZip(new FileInputStream(src)); if (src.endsWith("igpack.zip")) return readZip(new FileInputStream(src)); - Manager.FhirFormat fmt = ResourceChecker.checkIsResource(getContext(), isDebug(), src); + Manager.FhirFormat fmt = ResourceChecker.checkIsResource(getContext(), isDebug(), TextFile.fileToBytes(f), src, true); if (fmt != null) { Map res = new HashMap(); res.put(Utilities.changeFileExt(src, "." + fmt.getExtension()), TextFile.fileToBytesNCS(src)); @@ -470,7 +470,7 @@ public class IgLoader { else cnt = TextFile.streamToBytes(stream); - Manager.FhirFormat fmt = ResourceChecker.checkIsResource(getContext(), isDebug(), cnt, src); + Manager.FhirFormat fmt = ResourceChecker.checkIsResource(getContext(), isDebug(), cnt, src, true); if (fmt != null) { Map res = new HashMap(); res.put(Utilities.changeFileExt(src, "." + fmt.getExtension()), cnt); @@ -569,7 +569,7 @@ public class IgLoader { if (ff.isDirectory() && recursive) { res.putAll(scanDirectory(ff, true)); } else if (!ff.isDirectory() && !isIgnoreFile(ff)) { - Manager.FhirFormat fmt = ResourceChecker.checkIsResource(getContext(), isDebug(), ff.getAbsolutePath()); + Manager.FhirFormat fmt = ResourceChecker.checkIsResource(getContext(), isDebug(), TextFile.fileToBytes(ff), ff.getAbsolutePath(), true); if (fmt != null) { res.put(Utilities.changeFileExt(ff.getName(), "." + fmt.getExtension()), TextFile.fileToBytes(ff.getAbsolutePath())); } diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ResourceChecker.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ResourceChecker.java index 190beaf19..ab0c772f4 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ResourceChecker.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ResourceChecker.java @@ -2,16 +2,75 @@ package org.hl7.fhir.validation; import org.hl7.fhir.r5.context.SimpleWorkerContext; import org.hl7.fhir.r5.elementmodel.Manager; +import org.hl7.fhir.r5.elementmodel.SHCParser; +import org.hl7.fhir.r5.elementmodel.Manager.FhirFormat; +import org.hl7.fhir.r5.elementmodel.SHCParser.JWT; import org.hl7.fhir.r5.utils.structuremap.StructureMapUtilities; import org.hl7.fhir.utilities.TextFile; import org.hl7.fhir.utilities.Utilities; +import org.hl7.fhir.utilities.json.JSONUtil; +import org.hl7.fhir.utilities.json.JsonTrackingParser; + +import com.google.gson.JsonObject; import java.io.ByteArrayInputStream; import java.io.IOException; +import java.nio.charset.StandardCharsets; public class ResourceChecker { - protected static Manager.FhirFormat checkIsResource(SimpleWorkerContext context, boolean debug, byte[] cnt, String filename) { + +// protected static Manager.FhirFormat checkIsResource(SimpleWorkerContext context, boolean debug, String path) throws IOException { +// +// if (Utilities.existsInList(ext, "json")) +// return Manager.FhirFormat.JSON; +// if (Utilities.existsInList(ext, "map")) +// return Manager.FhirFormat.TEXT; +// if (Utilities.existsInList(ext, "txt")) +// return Manager.FhirFormat.TEXT; +// if (Utilities.existsInList(ext, "jwt", "jws")) +// return Manager.FhirFormat.SHC; +// +// return checkIsResource(context, debug, TextFile.fileToBytes(path), path); +// } + public static Manager.FhirFormat checkIsResource(SimpleWorkerContext context, boolean debug, byte[] cnt, String filename, boolean guessFromExtension) { System.out.println(" ..Detect format for " + filename); + if (guessFromExtension) { + String ext = Utilities.getFileExtension(filename); + if (Utilities.existsInList(ext, "xml")) { + return FhirFormat.XML; + } + if (Utilities.existsInList(ext, "ttl")) { + return FhirFormat.TURTLE; + } + if (Utilities.existsInList(ext, "map")) { + return Manager.FhirFormat.TEXT; + } + if (Utilities.existsInList(ext, "jwt", "jws")) { + return Manager.FhirFormat.SHC; + } + if (Utilities.existsInList(ext, "json")) { + // no, we have to look inside, and decide. + try { + JsonObject json = JsonTrackingParser.parseJson(cnt); + if (json.has("verifiableCredential")) { + return FhirFormat.SHC; + } + } catch (Exception e) { + } + return FhirFormat.JSON; + } + if (Utilities.existsInList(ext, "txt")) { + try { + String src = TextFile.bytesToString(cnt); + if (src.startsWith("shc:/")) { + return FhirFormat.SHC; + } + } catch (Exception e) { + } + return Manager.FhirFormat.TEXT; + } + } + try { Manager.parse(context, new ByteArrayInputStream(cnt), Manager.FhirFormat.JSON); return Manager.FhirFormat.JSON; @@ -36,6 +95,17 @@ public class ResourceChecker { System.out.println("Not Turtle: " + e.getMessage()); } } + try { + String s = new String(cnt, StandardCharsets.UTF_8); + if (s.startsWith("shc:/")) + s = SHCParser.decodeQRCode(s); + JWT jwt = SHCParser.decodeJWT(s); + return Manager.FhirFormat.SHC; + } catch (Exception e) { + if (debug) { + System.out.println("Not a smart health card: " + e.getMessage()); + } + } try { new StructureMapUtilities(context, null, null).parse(TextFile.bytesToString(cnt), null); return Manager.FhirFormat.TEXT; @@ -49,19 +119,5 @@ public class ResourceChecker { return null; } - protected static Manager.FhirFormat checkIsResource(SimpleWorkerContext context, boolean debug, String path) throws IOException { - String ext = Utilities.getFileExtension(path); - if (Utilities.existsInList(ext, "xml")) - return Manager.FhirFormat.XML; - if (Utilities.existsInList(ext, "json")) - return Manager.FhirFormat.JSON; - if (Utilities.existsInList(ext, "ttl")) - return Manager.FhirFormat.TURTLE; - if (Utilities.existsInList(ext, "map")) - return Manager.FhirFormat.TEXT; - if (Utilities.existsInList(ext, "txt")) - return Manager.FhirFormat.TEXT; - - return checkIsResource(context, debug, TextFile.fileToBytes(path), path); - } + } diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ValidationEngine.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ValidationEngine.java index 8c4cbc52f..78ba56a04 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ValidationEngine.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ValidationEngine.java @@ -21,6 +21,7 @@ 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.ObjectConverter; +import org.hl7.fhir.r5.elementmodel.SHCParser; import org.hl7.fhir.r5.formats.FormatUtilities; import org.hl7.fhir.r5.formats.IParser.OutputStyle; import org.hl7.fhir.r5.formats.JsonParser; @@ -293,7 +294,7 @@ public class ValidationEngine implements IValidatorResourceFetcher, IPackageInst // testing entry point public OperationOutcome validate(FhirFormat format, InputStream stream, List profiles) throws FHIRException, IOException, EOperationOutcome { List messages = new ArrayList(); - InstanceValidator validator = getValidator(); + InstanceValidator validator = getValidator(format); validator.validate(null, messages, stream, format, asSdList(profiles)); return ValidatorUtils.messagesToOutcome(messages, context, fhirPathEngine); } @@ -350,7 +351,7 @@ public class ValidationEngine implements IValidatorResourceFetcher, IPackageInst } public OperationOutcome validate(byte[] source, FhirFormat cntType, List profiles, List messages) throws FHIRException, IOException, EOperationOutcome { - InstanceValidator validator = getValidator(); + InstanceValidator validator = getValidator(cntType); validator.validate(null, messages, new ByteArrayInputStream(source), cntType, asSdList(profiles)); return ValidatorUtils.messagesToOutcome(messages, context, fhirPathEngine); @@ -361,7 +362,7 @@ public class ValidationEngine implements IValidatorResourceFetcher, IPackageInst if (doNative) { SchemaValidator.validateSchema(location, cntType, messages); } - InstanceValidator validator = getValidator(); + InstanceValidator validator = getValidator(cntType); validator.validate(null, messages, new ByteArrayInputStream(source), cntType, asSdList(profiles)); if (showTimes) { System.out.println(location + ": " + validator.reportTimes()); @@ -377,7 +378,7 @@ public class ValidationEngine implements IValidatorResourceFetcher, IPackageInst if (doNative) { SchemaValidator.validateSchema(location, cntType, messages); } - InstanceValidator validator = getValidator(); + InstanceValidator validator = getValidator(cntType); validator.setResourceIdRule(resourceIdRule); validator.setBestPracticeWarningLevel(bpWarnings); validator.setCheckDisplay(displayOption); @@ -393,7 +394,7 @@ public class ValidationEngine implements IValidatorResourceFetcher, IPackageInst public org.hl7.fhir.r5.elementmodel.Element transform(byte[] source, FhirFormat cntType, String mapUri) throws FHIRException, IOException { List outputs = new ArrayList<>(); StructureMapUtilities scu = new StructureMapUtilities(context, new TransformSupportServices(outputs, mapLog, context)); - org.hl7.fhir.r5.elementmodel.Element src = Manager.parse(context, new ByteArrayInputStream(source), cntType); + org.hl7.fhir.r5.elementmodel.Element src = Manager.parseSingle(context, new ByteArrayInputStream(source), cntType); StructureMap map = context.getTransform(mapUri); if (map == null) throw new Error("Unable to find map " + mapUri + " (Known Maps = " + context.listMapUrls() + ")"); org.hl7.fhir.r5.elementmodel.Element resource = getTargetResourceFromStructureMap(map); @@ -448,14 +449,14 @@ public class ValidationEngine implements IValidatorResourceFetcher, IPackageInst public void convert(String source, String output) throws FHIRException, IOException { Content cnt = igLoader.loadContent(source, "validate", false); - Element e = Manager.parse(context, new ByteArrayInputStream(cnt.focus), cnt.cntType); + Element e = Manager.parseSingle(context, new ByteArrayInputStream(cnt.focus), cnt.cntType); Manager.compose(context, e, new FileOutputStream(output), (output.endsWith(".json") ? FhirFormat.JSON : FhirFormat.XML), OutputStyle.PRETTY, null); } public String evaluateFhirPath(String source, String expression) throws FHIRException, IOException { Content cnt = igLoader.loadContent(source, "validate", false); - FHIRPathEngine fpe = this.getValidator().getFHIRPathEngine(); - Element e = Manager.parse(context, new ByteArrayInputStream(cnt.focus), cnt.cntType); + FHIRPathEngine fpe = this.getValidator(null).getFHIRPathEngine(); + Element e = Manager.parseSingle(context, new ByteArrayInputStream(cnt.focus), cnt.cntType); ExpressionNode exp = fpe.parse(expression); return fpe.evaluateToString(new ValidatorHostContext(context, e), e, e, e, exp); } @@ -490,7 +491,7 @@ public class ValidationEngine implements IValidatorResourceFetcher, IPackageInst context.dropResource(type, id); } - public InstanceValidator getValidator() { + public InstanceValidator getValidator(FhirFormat format) throws FHIRException, IOException { InstanceValidator validator = new InstanceValidator(context, null, null); validator.setHintAboutNonMustSupport(hintAboutNonMustSupport); validator.setAnyExtensionsAllowed(anyExtensionsAllowed); @@ -512,6 +513,10 @@ public class ValidationEngine implements IValidatorResourceFetcher, IPackageInst validator.getBundleValidationRules().addAll(bundleValidationRules); validator.getValidationControl().putAll(validationControl); validator.setQuestionnaireMode(questionnaireMode); + if (format == FhirFormat.SHC) { + igLoader.loadIg(getIgs(), getBinaries(), SHCParser.CURRENT_PACKAGE, true); + } + return validator; } @@ -617,7 +622,7 @@ public class ValidationEngine implements IValidatorResourceFetcher, IPackageInst public byte[] transformVersion(String source, String targetVer, FhirFormat format, Boolean canDoNative) throws FHIRException, IOException, Exception { Content cnt = igLoader.loadContent(source, "validate", false); - org.hl7.fhir.r5.elementmodel.Element src = Manager.parse(context, new ByteArrayInputStream(cnt.focus), cnt.cntType); + org.hl7.fhir.r5.elementmodel.Element src = Manager.parseSingle(context, new ByteArrayInputStream(cnt.focus), cnt.cntType); // if the src has a url, we try to use the java code if ((canDoNative == null && src.hasChild("url")) || (canDoNative != null && canDoNative)) { diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ValidatorCli.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ValidatorCli.java index b00b20903..adcd3bf30 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ValidatorCli.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ValidatorCli.java @@ -235,7 +235,7 @@ public class ValidatorCli { } System.out.println("Validating"); if (cliContext.getMode() == EngineMode.SCAN) { - Scanner validationScanner = new Scanner(validator.getContext(), validator.getValidator(), validator.getIgLoader(), validator.getFhirPathEngine()); + Scanner validationScanner = new Scanner(validator.getContext(), validator.getValidator(null), validator.getIgLoader(), validator.getFhirPathEngine()); validationScanner.validateScan(cliContext.getOutput(), cliContext.getSources()); } else { validationService.validateSources(cliContext, validator); 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 363155ca0..9a467c259 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 @@ -67,6 +67,7 @@ import org.hl7.fhir.r5.elementmodel.Manager; import org.hl7.fhir.r5.elementmodel.Manager.FhirFormat; import org.hl7.fhir.r5.elementmodel.ObjectConverter; import org.hl7.fhir.r5.elementmodel.ParserBase; +import org.hl7.fhir.r5.elementmodel.ParserBase.NamedElement; import org.hl7.fhir.r5.elementmodel.ParserBase.ValidationPolicy; import org.hl7.fhir.r5.elementmodel.XmlParser; import org.hl7.fhir.r5.formats.FormatUtilities; @@ -296,13 +297,13 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat try { Element e = new ObjectConverter(context).convert((Resource) item); setParents(e); - self.validateResource(new ValidatorHostContext(ctxt.getAppContext(), e), valerrors, e, e, sd, IdStatus.OPTIONAL, new NodeStack(context, e, validationLanguage)); + self.validateResource(new ValidatorHostContext(ctxt.getAppContext(), e), valerrors, e, e, sd, IdStatus.OPTIONAL, new NodeStack(context, null, e, validationLanguage)); } catch (IOException e1) { throw new FHIRException(e1); } } else if (item instanceof Element) { Element e = (Element) item; - self.validateResource(new ValidatorHostContext(ctxt.getAppContext(), e), valerrors, e, e, sd, IdStatus.OPTIONAL, new NodeStack(context, e, validationLanguage)); + self.validateResource(new ValidatorHostContext(ctxt.getAppContext(), e), valerrors, e, e, sd, IdStatus.OPTIONAL, new NodeStack(context, null, e, validationLanguage)); } else throw new NotImplementedException(context.formatMessage(I18nConstants.NOT_DONE_YET_VALIDATORHOSTSERVICESCONFORMSTOPROFILE_WHEN_ITEM_IS_NOT_AN_ELEMENT)); boolean ok = true; @@ -566,16 +567,28 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat ((XmlParser) parser).setAllowXsiLocation(allowXsiLocation); parser.setupValidation(ValidationPolicy.EVERYTHING, errors); long t = System.nanoTime(); - Element e; + List list = null; try { - e = parser.parse(stream); + list = parser.parse(stream); } catch (IOException e1) { throw new FHIRException(e1); } timeTracker.load(t); - if (e != null) - validate(appContext, errors, e, profiles); - return e; + if (list != null && !list.isEmpty()) { + String url = parser.getImpliedProfile(); + if (url != null) { + StructureDefinition sd = context.fetchResource(StructureDefinition.class, url); + if (sd == null) { + rule(errors, IssueType.NOTFOUND, "Payload", false, "Implied profile "+url+" not known to validator"); + } else { + profiles.add(sd); + } + } + for (NamedElement ne : list) { + validate(appContext, errors, ne.getName(), ne.getElement(), profiles); + } + } + return (list == null || list.isEmpty()) ? null : list.get(0).getElement(); // todo: this is broken, but fixing it really complicates things elsewhere, so we do this for now } @Override @@ -602,7 +615,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat throw new FHIRException(e1); } timeTracker.load(t); - validate(appContext, errors, e, profiles); + validate(appContext, errors, null, e, profiles); return e; } @@ -633,7 +646,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat } timeTracker.load(t); if (e != null) { - validate(appContext, errors, e, profiles); + validate(appContext, errors, null, e, profiles); } return e; } @@ -665,7 +678,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat } timeTracker.load(t); if (e != null) - validate(appContext, errors, e, profiles); + validate(appContext, errors, null, e, profiles); return e; } @@ -691,26 +704,26 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat Element e = parser.parse(object); timeTracker.load(t); if (e != null) - validate(appContext, errors, e, profiles); + validate(appContext, errors, null, e, profiles); return e; } @Override - public void validate(Object appContext, List errors, Element element) throws FHIRException { - validate(appContext, errors, element, new ArrayList<>()); + public void validate(Object appContext, List errors, String initialPath, Element element) throws FHIRException { + validate(appContext, errors, initialPath, element, new ArrayList<>()); } @Override - public void validate(Object appContext, List errors, Element element, String profile) throws FHIRException { + public void validate(Object appContext, List errors, String initialPath, Element element, String profile) throws FHIRException { ArrayList profiles = new ArrayList<>(); if (profile != null) { profiles.add(getSpecifiedProfile(profile)); } - validate(appContext, errors, element, profiles); + validate(appContext, errors, initialPath, element, profiles); } @Override - public void validate(Object appContext, List errors, Element element, List profiles) throws FHIRException { + public void validate(Object appContext, List errors, String path, Element element, List profiles) throws FHIRException { // this is the main entry point; all the other public entry points end up here coming here... // so the first thing to do is to clear the internal state fetchCache.clear(); @@ -724,14 +737,14 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat long t = System.nanoTime(); if (profiles == null || profiles.isEmpty()) { - validateResource(new ValidatorHostContext(appContext, element), errors, element, element, null, resourceIdRule, new NodeStack(context, element, validationLanguage).resetIds()); + validateResource(new ValidatorHostContext(appContext, element), errors, element, element, null, resourceIdRule, new NodeStack(context, path, element, validationLanguage).resetIds()); } else { for (StructureDefinition defn : profiles) { - validateResource(new ValidatorHostContext(appContext, element), errors, element, element, defn, resourceIdRule, new NodeStack(context, element, validationLanguage).resetIds()); + validateResource(new ValidatorHostContext(appContext, element), errors, element, element, defn, resourceIdRule, new NodeStack(context, path, element, validationLanguage).resetIds()); } } if (hintAboutNonMustSupport) { - checkElementUsage(errors, element, new NodeStack(context, element, validationLanguage)); + checkElementUsage(errors, element, new NodeStack(context, path, element, validationLanguage)); } errors.removeAll(messagesToRemove); timeTracker.overall(t); @@ -3387,7 +3400,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat rr.setResource(res.getMatch()); rr.setFocus(res.getMatch()); rr.setExternal(false); - rr.setStack(new NodeStack(context, hostContext, validationLanguage).push(res.getEntry(), res.getIndex(), res.getEntry().getProperty().getDefinition(), + rr.setStack(new NodeStack(context, null, hostContext, validationLanguage).push(res.getEntry(), res.getIndex(), res.getEntry().getProperty().getDefinition(), res.getEntry().getProperty().getDefinition()).push(res.getMatch(), -1, res.getMatch().getProperty().getDefinition(), res.getMatch().getProperty().getDefinition())); rr.getStack().qualifyPath(".ofType("+rr.getResource().fhirType()+")"); @@ -4873,6 +4886,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat } } int last = -1; + ElementInfo lastei = null; int lastSlice = -1; for (ElementInfo ei : children) { String sliceInfo = ""; @@ -4911,13 +4925,14 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat if (!ToolingExtensions.readBoolExtension(profile, "http://hl7.org/fhir/StructureDefinition/structuredefinition-xml-no-order")) { boolean ok = (ei.definition == null) || (ei.index >= last) || isXmlAttr; - rule(errors, IssueType.INVALID, ei.line(), ei.col(), ei.getPath(), ok, I18nConstants.VALIDATION_VAL_PROFILE_OUTOFORDER, profile.getUrl(), ei.getName()); + rule(errors, IssueType.INVALID, ei.line(), ei.col(), ei.getPath(), ok, I18nConstants.VALIDATION_VAL_PROFILE_OUTOFORDER, profile.getUrl(), ei.getName(), lastei == null ? "(null)" : lastei.getName()); } if (ei.slice != null && ei.index == last && ei.slice.getSlicing().getOrdered()) { rule(errors, IssueType.INVALID, ei.line(), ei.col(), ei.getPath(), (ei.definition == null) || (ei.sliceindex >= lastSlice) || isXmlAttr, I18nConstants.VALIDATION_VAL_PROFILE_SLICEORDER, profile.getUrl(), ei.getName()); } if (ei.definition == null || !isXmlAttr) { last = ei.index; + lastei = ei; } if (ei.slice != null) { lastSlice = ei.sliceindex; diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/utils/NodeStack.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/utils/NodeStack.java index d1f7fe45d..e753da6d7 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/utils/NodeStack.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/utils/NodeStack.java @@ -30,11 +30,11 @@ public class NodeStack { this.context = context; } - public NodeStack(IWorkerContext context, Element element, String validationLanguage) { + public NodeStack(IWorkerContext context, String initialPath, Element element, String validationLanguage) { this.context = context; ids = new HashMap<>(); this.element = element; - literalPath = element.getPath(); + literalPath = (initialPath == null ? "" : initialPath+".") + element.getPath(); workingLang = validationLanguage; if (!element.getName().equals(element.fhirType())) { logicalPaths = new ArrayList<>(); diff --git a/org.hl7.fhir.validation/src/test/java/org/hl7/fhir/validation/tests/ValidationTests.java b/org.hl7.fhir.validation/src/test/java/org/hl7/fhir/validation/tests/ValidationTests.java index 30ee70d92..5dbdb0028 100644 --- a/org.hl7.fhir.validation/src/test/java/org/hl7/fhir/validation/tests/ValidationTests.java +++ b/org.hl7.fhir.validation/src/test/java/org/hl7/fhir/validation/tests/ValidationTests.java @@ -1,11 +1,13 @@ package org.hl7.fhir.validation.tests; +import java.io.ByteArrayInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.net.MalformedURLException; import java.net.URL; import java.net.URLConnection; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -31,6 +33,7 @@ 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.ObjectConverter; +import org.hl7.fhir.r5.elementmodel.SHCParser; import org.hl7.fhir.r5.formats.JsonParser; import org.hl7.fhir.r5.formats.XmlParser; import org.hl7.fhir.r5.model.Base; @@ -162,8 +165,11 @@ public class ValidationTests implements IEvaluationContext, IValidatorResourceFe if (content.has("use-test") && !content.get("use-test").getAsBoolean()) return; - String testCaseContent = TestingUtilities.loadTestResource("validator", JSONUtil.str(content, "file")); - InstanceValidator val = vCurr.getValidator(); + byte[] testCaseContent = TestingUtilities.loadTestResource("validator", JSONUtil.str(content, "file")).getBytes(StandardCharsets.UTF_8); + // load and process content + FhirFormat fmt = determineFormat(content, testCaseContent); + + InstanceValidator val = vCurr.getValidator(fmt); val.setWantCheckSnapshotUnchanged(true); val.getContext().setClientRetryCount(4); val.setDebug(false); @@ -236,13 +242,11 @@ public class ValidationTests implements IEvaluationContext, IValidatorResourceFe if (content.has("security-checks")) { val.setSecurityChecks(content.get("security-checks").getAsBoolean()); } + if (content.has("logical")==false) { val.setAssumeValidRestReferences(content.has("assumeValidRestReferences") ? content.get("assumeValidRestReferences").getAsBoolean() : false); System.out.println(String.format("Start Validating (%d to set up)", (System.nanoTime() - setup) / 1000000)); - if (JSONUtil.str(content, "file").endsWith(".json")) - val.validate(null, errors, IOUtils.toInputStream(testCaseContent, Charsets.UTF_8), FhirFormat.JSON); - else - val.validate(null, errors, IOUtils.toInputStream(testCaseContent, Charsets.UTF_8), FhirFormat.XML); + val.validate(null, errors, new ByteArrayInputStream(testCaseContent), fmt); System.out.println(val.reportTimes()); checkOutcomes(errors, content, null, name); } @@ -281,10 +285,7 @@ public class ValidationTests implements IEvaluationContext, IValidatorResourceFe } val.setAssumeValidRestReferences(profile.has("assumeValidRestReferences") ? profile.get("assumeValidRestReferences").getAsBoolean() : false); List errorsProfile = new ArrayList(); - if (JSONUtil.str(content, "file").endsWith(".json")) - val.validate(null, errorsProfile, IOUtils.toInputStream(testCaseContent, Charsets.UTF_8), FhirFormat.JSON, asSdList(sd)); - else - val.validate(null, errorsProfile, IOUtils.toInputStream(testCaseContent, Charsets.UTF_8), FhirFormat.XML, asSdList(sd)); + val.validate(null, errorsProfile, new ByteArrayInputStream(testCaseContent), fmt, asSdList(sd)); System.out.println(val.reportTimes()); checkOutcomes(errorsProfile, profile, filename, name); } @@ -309,7 +310,7 @@ public class ValidationTests implements IEvaluationContext, IValidatorResourceFe } } List errorsLogical = new ArrayList(); - Element le = val.validate(null, errorsLogical, IOUtils.toInputStream(testCaseContent, Charsets.UTF_8), (name.endsWith(".json")) ? FhirFormat.JSON : FhirFormat.XML); + Element le = val.validate(null, errorsLogical, new ByteArrayInputStream(testCaseContent), fmt); if (logical.has("expressions")) { FHIRPathEngine fp = new FHIRPathEngine(val.getContext()); for (JsonElement e : logical.getAsJsonArray("expressions")) { @@ -321,6 +322,11 @@ public class ValidationTests implements IEvaluationContext, IValidatorResourceFe } } + private FhirFormat determineFormat(JsonObject config, byte[] cnt) throws IOException { + String name = JSONUtil.str(config, "file"); + return org.hl7.fhir.validation.ResourceChecker.checkIsResource(vCurr.getContext(), true, cnt, name, !JSONUtil.bool(config, "guess-format")); + } + private List asSdList(StructureDefinition sd) { List res = new ArrayList(); res.add(sd); @@ -482,16 +488,16 @@ public class ValidationTests implements IEvaluationContext, IValidatorResourceFe if (url.equals("Patient/test")) { res = new ObjectConverter(TestingUtilities.context(version)).convert(new Patient()); } else if (TestingUtilities.findTestResource("validator", url.replace("/", "-").toLowerCase() + ".json")) { - res = Manager.makeParser(TestingUtilities.context(version), FhirFormat.JSON).parse(TestingUtilities.loadTestResourceStream("validator", url.replace("/", "-").toLowerCase() + ".json")); + res = Manager.makeParser(TestingUtilities.context(version), FhirFormat.JSON).parseSingle(TestingUtilities.loadTestResourceStream("validator", url.replace("/", "-").toLowerCase() + ".json")); } else if (TestingUtilities.findTestResource("validator", url.replace("/", "-").toLowerCase() + ".xml")) { - res = Manager.makeParser(TestingUtilities.context(version), FhirFormat.XML).parse(TestingUtilities.loadTestResourceStream("validator", url.replace("/", "-").toLowerCase() + ".xml")); + res = Manager.makeParser(TestingUtilities.context(version), FhirFormat.XML).parseSingle(TestingUtilities.loadTestResourceStream("validator", url.replace("/", "-").toLowerCase() + ".xml")); } if (res == null && url.contains("/")) { String tail = url.substring(url.indexOf("/") + 1); if (TestingUtilities.findTestResource("validator", tail.replace("/", "-").toLowerCase() + ".json")) { - res = Manager.makeParser(TestingUtilities.context(version), FhirFormat.JSON).parse(TestingUtilities.loadTestResourceStream("validator", tail.replace("/", "-").toLowerCase() + ".json")); + res = Manager.makeParser(TestingUtilities.context(version), FhirFormat.JSON).parseSingle(TestingUtilities.loadTestResourceStream("validator", tail.replace("/", "-").toLowerCase() + ".json")); } else if (TestingUtilities.findTestResource("validator", tail.replace("/", "-").toLowerCase() + ".xml")) { - res = Manager.makeParser(TestingUtilities.context(version), FhirFormat.XML).parse(TestingUtilities.loadTestResourceStream("validator", tail.replace("/", "-").toLowerCase() + ".xml")); + res = Manager.makeParser(TestingUtilities.context(version), FhirFormat.XML).parseSingle(TestingUtilities.loadTestResourceStream("validator", tail.replace("/", "-").toLowerCase() + ".xml")); } } return res;