Smart Health Cards support in validator

This commit is contained in:
Grahame Grieve 2021-09-15 21:20:29 +10:00
parent 899eb788f3
commit 8c148469d7
24 changed files with 593 additions and 104 deletions

View File

@ -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<NamedElement> parse(InputStream stream) throws IOException, FHIRException {
// if we're parsing at this point, then we're going to use the custom parser
List<NamedElement> res = new ArrayList<>();
map = new IdentityHashMap<JsonElement, LocationData>();
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<JsonElement, LocationData> map) throws FHIRException {

View File

@ -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<NamedElement> 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;

View File

@ -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<NamedElement> 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 {

View File

@ -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<NamedElement> parse(InputStream stream) throws IOException, FHIRFormatError, DefinitionException, FHIRException;
public Element parseSingle(InputStream stream) throws IOException, FHIRFormatError, DefinitionException, FHIRException {
List<NamedElement> 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;
}
}

View File

@ -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<JsonElement, LocationData> map;
private List<String> types = new ArrayList<>();
public SHCParser(IWorkerContext context) {
super(context);
jsonParser = new JsonParser(context);
}
public List<NamedElement> parse(InputStream stream) throws IOException, FHIRFormatError, DefinitionException, FHIRException {
List<NamedElement> res = new ArrayList<>();
String src = TextFile.streamToString(stream).trim();
List<String> 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<String, JsonElement> 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<JsonElement, LocationData> 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();
}
}
}

View File

@ -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"));

View File

@ -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<NamedElement> parse(InputStream input) throws IOException, FHIRException {
List<NamedElement> 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 {

View File

@ -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<NamedElement> 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<NamedElement> res = new ArrayList<>();
res.add(new NamedElement(null, message));
return res;
}
private void preDecode(VerticalBarParserReader reader) throws FHIRException {

View File

@ -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<NamedElement> parse(InputStream stream) throws FHIRFormatError, DefinitionException, FHIRException, IOException {
List<NamedElement> 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;
}

View File

@ -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<ValidationMessage> errors, org.hl7.fhir.r5.elementmodel.Element element) throws FHIRException;
void validate(Object Context, List<ValidationMessage> errors, org.hl7.fhir.r5.elementmodel.Element element, String profile) throws FHIRException;
void validate(Object Context, List<ValidationMessage> errors, org.hl7.fhir.r5.elementmodel.Element element, List<StructureDefinition> profiles) throws FHIRException;
void validate(Object Context, List<ValidationMessage> errors, String initialPath, org.hl7.fhir.r5.elementmodel.Element element) throws FHIRException;
void validate(Object Context, List<ValidationMessage> errors, String initialPath, org.hl7.fhir.r5.elementmodel.Element element, String profile) throws FHIRException;
void validate(Object Context, List<ValidationMessage> errors, String initialPath, org.hl7.fhir.r5.elementmodel.Element element, List<StructureDefinition> profiles) throws FHIRException;
org.hl7.fhir.r5.elementmodel.Element validate(Object Context, List<ValidationMessage> errors, InputStream stream, FhirFormat format) throws FHIRException;
org.hl7.fhir.r5.elementmodel.Element validate(Object Context, List<ValidationMessage> errors, InputStream stream, FhirFormat format, String profile) throws FHIRException;

View File

@ -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<Element> title = xml.getChildrenByName("title");

View File

@ -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"));

View File

@ -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<Element> concept = xml.getChildrenByName("concept");
assertTrue(concept!=null && concept.size()==1);

View File

@ -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) {

View File

@ -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();

View File

@ -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;

View File

@ -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";
}
}

View File

@ -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<String, byte[]> res = new HashMap<String, byte[]>();
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<String, byte[]> res = new HashMap<String, byte[]>();
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<String, byte[]> res = new HashMap<String, byte[]>();
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()));
}

View File

@ -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);
}
}

View File

@ -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<String> profiles) throws FHIRException, IOException, EOperationOutcome {
List<ValidationMessage> messages = new ArrayList<ValidationMessage>();
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<String> profiles, List<ValidationMessage> 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<Base> 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)) {

View File

@ -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);

View File

@ -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<NamedElement> 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<ValidationMessage> errors, Element element) throws FHIRException {
validate(appContext, errors, element, new ArrayList<>());
public void validate(Object appContext, List<ValidationMessage> errors, String initialPath, Element element) throws FHIRException {
validate(appContext, errors, initialPath, element, new ArrayList<>());
}
@Override
public void validate(Object appContext, List<ValidationMessage> errors, Element element, String profile) throws FHIRException {
public void validate(Object appContext, List<ValidationMessage> errors, String initialPath, Element element, String profile) throws FHIRException {
ArrayList<StructureDefinition> 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<ValidationMessage> errors, Element element, List<StructureDefinition> profiles) throws FHIRException {
public void validate(Object appContext, List<ValidationMessage> errors, String path, Element element, List<StructureDefinition> 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;

View File

@ -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<>();

View File

@ -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<ValidationMessage> errorsProfile = new ArrayList<ValidationMessage>();
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<ValidationMessage> errorsLogical = new ArrayList<ValidationMessage>();
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<StructureDefinition> asSdList(StructureDefinition sd) {
List<StructureDefinition> res = new ArrayList<StructureDefinition>();
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;