Fix validation issues for StructureDefinitions (#396)

This commit is contained in:
Grahame Grieve 2020-12-05 08:11:52 +11:00 committed by GitHub
parent 118c03590f
commit 7de14f172e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 157 additions and 20 deletions

View File

@ -109,7 +109,7 @@ public interface IResourceValidator {
Element fetch(Object appContext, String url) throws FHIRFormatError, DefinitionException, FHIRException, IOException;
ReferenceValidationPolicy validationPolicy(Object appContext, String path, String url);
boolean resolveURL(Object appContext, String path, String url) throws IOException, FHIRException;
boolean resolveURL(Object appContext, String path, String url, String type) throws IOException, FHIRException;
byte[] fetchRaw(String url) throws MalformedURLException, IOException; // for attachment checking

View File

@ -364,6 +364,7 @@ public class VersionUtilities {
res.add("TestScript");
}
if (isR3Ver(version)) {
res.add("CodeSystem");
res.add("CapabilityStatement");
res.add("StructureDefinition");
res.add("ImplementationGuide");
@ -388,6 +389,7 @@ public class VersionUtilities {
}
if (isR4Ver(version)) {
res.add("CodeSystem");
res.add("ActivityDefinition");
res.add("CapabilityStatement");
res.add("ChargeItemDefinition");
@ -419,7 +421,7 @@ public class VersionUtilities {
res.add("ValueSet");
}
if (isR5Ver(version)) {
if (isR5Ver(version) || "current".equals(version)) {
res.add("ActivityDefinition");
res.add("CapabilityStatement");

View File

@ -350,6 +350,7 @@ public class I18nConstants {
public static final String SD_ED_TYPE_PROFILE_UNKNOWN = "SD_ED_TYPE_PROFILE_UNKNOWN";
public static final String SD_ED_TYPE_PROFILE_NOTYPE = "SD_ED_TYPE_PROFILE_NOTYPE";
public static final String SD_ED_TYPE_PROFILE_WRONG = "SD_ED_TYPE_PROFILE_WRONG";
public static final String SD_ED_TYPE_PROFILE_WRONG_TARGET = "SD_ED_TYPE_PROFILE_WRONG_TARGET";
public static final String SD_ED_TYPE_NO_TARGET_PROFILE = "SD_ED_TYPE_NO_TARGET_PROFILE";
public static final String SEARCHPARAMETER_BASE_WRONG = "SEARCHPARAMETER_BASE_WRONG";
public static final String SEARCHPARAMETER_EXP_WRONG = "SEARCHPARAMETER_EXP_WRONG";
@ -593,6 +594,11 @@ public class I18nConstants {
public static final String XHTML_URL_EMPTY = "XHTML_URL_EMPTY";
public static final String XHTML_URL_INVALID = "XHTML_URL_INVALID";
public static final String XHTML_URL_INVALID_CHARS = "XHTML_URL_INVALID_CHARS";
public static final String XHTML_URL_DATA_NO_DATA = "XHTML_URL_DATA_NO_DATA";
public static final String XHTML_URL_DATA_DATA_INVALID_COMMA = "XHTML_URL_DATA_DATA_INVALID_COMMA";
public static final String XHTML_URL_DATA_DATA_INVALID = "XHTML_URL_DATA_DATA_INVALID";
public static final String XHTML_URL_DATA_MIMETYPE = "XHTML_URL_DATA_MIMETYPE";
public static final String XHTML_XHTML_ATTRIBUTE_ILLEGAL = "XHTML_XHTML_Attribute_Illegal";
public static final String XHTML_XHTML_DOCTYPE_ILLEGAL = "XHTML_XHTML_DOCTYPE_ILLEGAL";
public static final String XHTML_XHTML_ELEMENT_ILLEGAL = "XHTML_XHTML_Element_Illegal";

View File

@ -443,7 +443,7 @@ documentmsg = (document)
xml_attr_value_invalid = The XML Attribute {0} has an illegal character
xml_encoding_invalid = The XML encoding is invalid (must be UTF-8)
xml_stated_encoding_invalid = The XML encoding stated in the header is invalid (must be ''UTF-8'' if stated)
XHTML_URL_INVALID = The URL {0} is not valid ({1})
XHTML_URL_INVALID = The URL is not valid because ''({1})'' : {0}
MEASURE_MR_GRP_NO_CODE = Group should have a code that matches the group definition in the measure
MEASURE_MR_GRP_UNK_CODE = The code for this group has no match in the measure definition
MEASURE_MR_GRP_DUPL_CODE = The code for this group is duplicated with another group
@ -619,8 +619,12 @@ Unable_to_connect_to_terminology_server = Unable to connect to terminology serve
SD_ED_TYPE_PROFILE_UNKNOWN = Unable to resolve profile {0}
SD_ED_TYPE_PROFILE_NOTYPE = Found profile {0}, but unable to determine the type it applies it
SD_ED_TYPE_PROFILE_WRONG = Profile {0} is for type {1}, but the {3} element has type {2}
SD_ED_TYPE_PROFILE_WRONG_TARGET = Profile {0} is for type {1}, which is not a {4} (which is required because the {3} element has type {2})
SD_ED_TYPE_NO_TARGET_PROFILE = Type {0} does not allow for target Profiles
TERMINOLOGY_TX_NOSVC_BOUND_REQ = Could not confirm that the codes provided are from the required value set {0} because there is no terminology service
TERMINOLOGY_TX_NOSVC_BOUND_EXT = Could not confirm that the codes provided are from the extensible value set {0} because there is no terminology service
ARRAY_CANNOT_BE_EMPTY = Array cannot be empty - the property should not be present if it has no values
XHTML_URL_DATA_NO_DATA = No data found in data: URL
XHTML_URL_DATA_DATA_INVALID_COMMA = Comma found in data portion of data URL: {0}
XHTML_URL_DATA_DATA_INVALID = The data should be valid base64 content for a data: URL: {0}
XHTML_URL_DATA_MIMETYPE = The mimetype potion of the data: URL is not valid ({1}) in URL: {0}

View File

@ -104,6 +104,7 @@ import org.hl7.fhir.utilities.TimeTracker;
import org.hl7.fhir.utilities.Utilities;
import org.hl7.fhir.utilities.VersionUtilities;
import org.hl7.fhir.utilities.i18n.I18nConstants;
import org.hl7.fhir.utilities.json.JSONUtil;
import org.hl7.fhir.utilities.json.JsonTrackingParser;
import org.hl7.fhir.utilities.npm.FilesystemPackageCacheManager;
import org.hl7.fhir.utilities.npm.NpmPackage;
@ -114,6 +115,7 @@ import org.hl7.fhir.utilities.validation.ValidationMessage.IssueSeverity;
import org.hl7.fhir.utilities.validation.ValidationMessage.IssueType;
import org.hl7.fhir.utilities.validation.ValidationMessage.Source;
import org.hl7.fhir.utilities.xhtml.XhtmlComposer;
import org.hl7.fhir.utilities.xml.XMLUtil;
import org.hl7.fhir.validation.BaseValidator.ValidationControl;
import org.hl7.fhir.validation.ValidationEngine.ValidationRecord;
import org.hl7.fhir.validation.cli.model.ScanOutputItem;
@ -122,6 +124,9 @@ import org.hl7.fhir.validation.cli.utils.*;
import org.hl7.fhir.validation.instance.InstanceValidator;
import org.w3c.dom.Document;
import org.xml.sax.SAXException;
import org.xmlpull.v1.builder.XmlDocument;
import com.google.gson.JsonObject;
/*
@ -1310,6 +1315,22 @@ public class ValidationEngine implements IValidatorResourceFetcher, IPackageInst
if (s.contains("http://hl7.org/fhir/1.4")) {
versions.see("1.4", "Profile in "+ref);
}
try {
if (s.startsWith("{")) {
JsonObject json = JsonTrackingParser.parse(s, null);
if (json.has("fhirVersion")) {
versions.see(VersionUtilities.getMajMin(JSONUtil.str(json, "fhirVersion")), "fhirVersion in "+ref);
}
} else {
Document doc = parseXml(cnt.focus);
String v = XMLUtil.getNamedChildValue(doc.getDocumentElement(), "fhirVersion");
if (v != null) {
versions.see(VersionUtilities.getMajMin(v), "fhirVersion in "+ref);
}
}
} catch (Exception e) {
// nothing
}
}
}
@ -1883,7 +1904,7 @@ public class ValidationEngine implements IValidatorResourceFetcher, IPackageInst
}
@Override
public boolean resolveURL(Object appContext, String path, String url) throws IOException, FHIRException {
public boolean resolveURL(Object appContext, String path, String url, String type) throws IOException, FHIRException {
if (!url.startsWith("http://") && !url.startsWith("https://")) { // ignore these
return true;
}
@ -1898,7 +1919,7 @@ public class ValidationEngine implements IValidatorResourceFetcher, IPackageInst
return true;
}
if (fetcher != null) {
return fetcher.resolveURL(appContext, path, url);
return fetcher.resolveURL(appContext, path, url, type);
};
return false;
}

View File

@ -3,6 +3,8 @@ package org.hl7.fhir.validation.cli.services;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import org.hl7.fhir.convertors.txClient.TerminologyClientFactory;
@ -18,10 +20,16 @@ import org.hl7.fhir.r5.utils.IResourceValidator.ReferenceValidationPolicy;
import org.hl7.fhir.utilities.Utilities;
import org.hl7.fhir.utilities.VersionUtilities;
import org.hl7.fhir.utilities.VersionUtilities.VersionURLInfo;
import org.hl7.fhir.utilities.json.JSONUtil;
import org.hl7.fhir.utilities.json.JsonTrackingParser;
import org.hl7.fhir.utilities.npm.BasePackageCacheManager;
import org.hl7.fhir.utilities.npm.FilesystemPackageCacheManager;
import org.hl7.fhir.utilities.npm.NpmPackage;
import com.google.gson.JsonObject;
import ca.uhn.fhir.util.JsonUtil;
public class StandAloneValidatorFetcher implements IValidatorResourceFetcher {
public interface IPackageInstaller {
@ -29,6 +37,7 @@ public class StandAloneValidatorFetcher implements IValidatorResourceFetcher {
void loadPackage(String id, String ver) throws IOException, FHIRException;
}
List<String> mappingsUris = new ArrayList<>();
private FilesystemPackageCacheManager pcm;
private IWorkerContext context;
private IPackageInstaller installer;
@ -51,11 +60,19 @@ public class StandAloneValidatorFetcher implements IValidatorResourceFetcher {
}
@Override
public boolean resolveURL(Object appContext, String path, String url) throws IOException, FHIRException {
public boolean resolveURL(Object appContext, String path, String url, String type) throws IOException, FHIRException {
if (!Utilities.isAbsoluteUrl(url)) {
return false;
}
if (url.contains("|")) {
url = url.substring(0, url.lastIndexOf("|"));
}
if (type.equals("uri") && isMappingUri(url)) {
return true;
}
// if we've got to here, it's a reference to a FHIR URL. We're going to try to resolve it on the fly
String pid = null;
String ver = null;
@ -94,10 +111,70 @@ public class StandAloneValidatorFetcher implements IValidatorResourceFetcher {
}
}
// we don't bother with urls outside fhir space in the standalone validator - we assume they are valid
return !url.startsWith("http://hl7.org/fhir");
}
private boolean isMappingUri(String url) {
if (mappingsUris.isEmpty()) {
JsonObject json;
try {
json = JsonTrackingParser.fetchJson("http://hl7.org/fhir/mappingspaces.json");
for (JsonObject ms : JSONUtil.objects(json, "spaces")) {
mappingsUris.add(JSONUtil.str(ms, "url"));
}
} catch (IOException e) {
// frozen R4 list
mappingsUris.add("http://hl7.org/fhir/fivews");
mappingsUris.add("http://hl7.org/fhir/workflow");
mappingsUris.add("http://hl7.org/fhir/interface");
mappingsUris.add("http://hl7.org/v2");
mappingsUris.add("http://loinc.org");
mappingsUris.add("http://snomed.org/attributebinding");
mappingsUris.add("http://snomed.info/conceptdomain");
mappingsUris.add("http://hl7.org/v3/cda");
mappingsUris.add("http://hl7.org/v3");
mappingsUris.add("http://nema.org/dicom");
mappingsUris.add("http://w3.org/vcard");
mappingsUris.add("http://ihe.net/xds");
mappingsUris.add("http://www.w3.org/ns/prov");
mappingsUris.add("http://ietf.org/rfc/2445");
mappingsUris.add("http://www.omg.org/spec/ServD/1.0/");
mappingsUris.add("http://metadata-standards.org/11179/");
mappingsUris.add("http://ihe.net/data-element-exchange");
mappingsUris.add("http://openehr.org");
mappingsUris.add("http://siframework.org/ihe-sdc-profile");
mappingsUris.add("http://siframework.org/cqf");
mappingsUris.add("http://www.cdisc.org/define-xml");
mappingsUris.add("http://www.cda-adc.ca/en/services/cdanet/");
mappingsUris.add("http://www.pharmacists.ca/");
mappingsUris.add("http://www.healthit.gov/quality-data-model");
mappingsUris.add("http://hl7.org/orim");
mappingsUris.add("http://hl7.org/fhir/w5");
mappingsUris.add("http://hl7.org/fhir/logical");
mappingsUris.add("http://hl7.org/fhir/auditevent");
mappingsUris.add("http://hl7.org/fhir/provenance");
mappingsUris.add("http://hl7.org/qidam");
mappingsUris.add("http://cap.org/ecc");
mappingsUris.add("http://fda.gov/UDI");
mappingsUris.add("http://hl7.org/fhir/object-implementation");
mappingsUris.add("http://github.com/MDMI/ReferentIndexContent");
mappingsUris.add("http://ncpdp.org/SCRIPT10_6");
mappingsUris.add("http://clinicaltrials.gov");
mappingsUris.add("http://hl7.org/fhir/rr");
mappingsUris.add("http://www.hl7.org/v3/PORX_RM020070UV");
mappingsUris.add("https://bridgmodel.nci.nih.gov");
mappingsUris.add("http://hl7.org/fhir/composition");
mappingsUris.add("http://hl7.org/fhir/documentreference");
mappingsUris.add("https://en.wikipedia.org/wiki/Identification_of_medicinal_products");
mappingsUris.add("urn:iso:std:iso:11073:10201");
mappingsUris.add("urn:iso:std:iso:11073:10207");
}
}
return mappingsUris.contains(url);
}
private String findBaseUrl(String url) {
String[] p = url.split("\\/");
for (int i = 1; i< p.length; i++) {

View File

@ -1927,11 +1927,11 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
// for now, no validation. Need to think about authority.
} else {
// now, do we check the URI target?
if (fetcher != null) {
if (fetcher != null && !type.equals("uuid")) {
boolean found;
try {
found = isDefinitionURL(url) || (allowExamples && (url.contains("example.org") || url.contains("acme.com")) || url.contains("acme.org")) || (url.startsWith("http://hl7.org/fhir/tools")) || fetcher.resolveURL(appContext, path, url) ||
SpecialExtensions.isKnownExtension(url) || isXverUrl(url);
found = isDefinitionURL(url) || (allowExamples && (url.contains("example.org") || url.contains("acme.com")) || url.contains("acme.org")) || (url.startsWith("http://hl7.org/fhir/tools")) ||
SpecialExtensions.isKnownExtension(url) || isXverUrl(url) || fetcher.resolveURL(appContext, path, url, type);
} catch (IOException e1) {
found = false;
}
@ -2228,19 +2228,46 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
return context.formatMessage(I18nConstants.XHTML_URL_EMPTY);
}
Set<Character> invalidChars = new HashSet<>();
for (char ch : value.toCharArray()) {
if (!(Character.isDigit(ch) || Character.isAlphabetic(ch) || Utilities.existsInList(ch, ';', '?', ':', '@', '&', '=', '+', '$', '.', ',', '/', '%', '-', '_', '~', '#', '[', ']', '!', '\'', '(', ')', '*' ))) {
invalidChars.add(ch);
if (value.startsWith("data:")) {
String[] p = value.substring(5).split("\\,");
if (p.length < 2) {
return context.formatMessage(I18nConstants.XHTML_URL_DATA_NO_DATA, value);
} else if (p.length > 2) {
return context.formatMessage(I18nConstants.XHTML_URL_DATA_DATA_INVALID_COMMA, value);
} else if (!p[0].endsWith(";base64") || !isValidBase64(p[1])) {
return context.formatMessage(I18nConstants.XHTML_URL_DATA_DATA_INVALID, value);
} else {
if (p[0].startsWith(" ")) {
p[0] = p[0].trim();
}
String mMsg = checkValidMimeType(p[0].substring(0, p[0].lastIndexOf(";")));
if (mMsg != null) {
return context.formatMessage(I18nConstants.XHTML_URL_DATA_MIMETYPE, value, mMsg);
}
}
}
if (invalidChars.isEmpty()) {
return null;
} else {
return context.formatMessage(I18nConstants.XHTML_URL_INVALID_CHARS, invalidChars.toString());
Set<Character> invalidChars = new HashSet<>();
for (char ch : value.toCharArray()) {
if (!(Character.isDigit(ch) || Character.isAlphabetic(ch) || Utilities.existsInList(ch, ';', '?', ':', '@', '&', '=', '+', '$', '.', ',', '/', '%', '-', '_', '~', '#', '[', ']', '!', '\'', '(', ')', '*' ))) {
invalidChars.add(ch);
}
}
if (invalidChars.isEmpty()) {
return null;
} else {
return context.formatMessage(I18nConstants.XHTML_URL_INVALID_CHARS, invalidChars.toString());
}
}
}
private String checkValidMimeType(String mt) {
if (!mt.matches("^(\\w+|\\*)\\/(\\w+|\\*)((;\\s*(\\w+)=\\s*(\\S+))?)$")) {
return "Mime type invalid";
}
return null;
}
private void checkInnerNS(List<ValidationMessage> errors, Element e, String path, List<XhtmlNode> list) {
for (XhtmlNode node : list) {
if (node.getNodeType() == NodeType.Element) {

View File

@ -223,7 +223,7 @@ public class StructureDefinitionValidator extends BaseValidator {
if (t == null) {
rule(errors, IssueType.EXCEPTION, stack.getLiteralPath(), code.equals(t), I18nConstants.SD_ED_TYPE_PROFILE_NOTYPE, p);
} else {
rule(errors, IssueType.EXCEPTION, stack.getLiteralPath(), sd.getKind() == StructureDefinitionKind.RESOURCE, I18nConstants.SD_ED_TYPE_PROFILE_WRONG, p, t, code, path);
rule(errors, IssueType.EXCEPTION, stack.getLiteralPath(), sd.getKind() == StructureDefinitionKind.RESOURCE, I18nConstants.SD_ED_TYPE_PROFILE_WRONG_TARGET, p, t, code, path, "Resource");
}
}
} else if (code.equals("canonical")) {
@ -232,7 +232,7 @@ public class StructureDefinitionValidator extends BaseValidator {
if (t == null) {
rule(errors, IssueType.EXCEPTION, stack.getLiteralPath(), code.equals(t), I18nConstants.SD_ED_TYPE_PROFILE_NOTYPE, p);
} else {
rule(errors, IssueType.EXCEPTION, stack.getLiteralPath(), VersionUtilities.getCanonicalResourceNames(context.getVersion()).contains(t), I18nConstants.SD_ED_TYPE_PROFILE_WRONG, p, t, code, path);
rule(errors, IssueType.EXCEPTION, stack.getLiteralPath(), VersionUtilities.getCanonicalResourceNames(context.getVersion()).contains(t), I18nConstants.SD_ED_TYPE_PROFILE_WRONG_TARGET, p, t, code, path, "Canonical Resource");
}
}
} else {

View File

@ -490,7 +490,7 @@ public class ValidationTests implements IEvaluationContext, IValidatorResourceFe
}
@Override
public boolean resolveURL(Object appContext, String path, String url) throws IOException, FHIRException {
public boolean resolveURL(Object appContext, String path, String url, String type) throws IOException, FHIRException {
return !url.contains("example.org") && !url.startsWith("http://hl7.org/fhir/invalid");
}