* fix fatal NPE validating bundles when resource is missing
* fix tests for R5 changes
This commit is contained in:
parent
463a557402
commit
d688f9254d
|
@ -329,8 +329,6 @@ public class PackageCacheManager {
|
|||
}
|
||||
|
||||
private InputStreamWithSrc loadFromPackageServer(String id, String v) {
|
||||
// release hack:
|
||||
if ("4.4.0".equals(v)) {v = "4.2.0"; };
|
||||
PackageClient pc = new PackageClient(PRIMARY_SERVER);
|
||||
String u = null;
|
||||
InputStream stream;
|
||||
|
@ -861,6 +859,16 @@ public class PackageCacheManager {
|
|||
return null;
|
||||
}
|
||||
|
||||
public List<String> listPackages() {
|
||||
List<String> res = new ArrayList<>();
|
||||
for (File f : new File(cacheFolder).listFiles()) {
|
||||
if (f.isDirectory() && f.getName().contains("#")) {
|
||||
res.add(f.getName());
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
|
||||
|
||||
//public List<String> getUrls() throws IOException {
|
||||
|
|
|
@ -14,11 +14,10 @@ import java.io.IOException;
|
|||
public class PackageCacheTests {
|
||||
|
||||
@Test
|
||||
@Disabled // This test is currently set to always fail.
|
||||
public void testPath() throws IOException {
|
||||
PackageCacheManager cache = new PackageCacheManager(true, ToolsVersion.TOOLS_VERSION);
|
||||
cache.clear();
|
||||
Assertions.assertTrue(false);
|
||||
Assertions.assertTrue(cache.listPackages().isEmpty());
|
||||
NpmPackage npm = cache.loadPackage("hl7.fhir.pubpack", "0.0.3");
|
||||
npm.loadAllFiles();
|
||||
Assertions.assertNotNull(npm);
|
||||
|
@ -31,5 +30,6 @@ public class PackageCacheTests {
|
|||
npm.save(dir);
|
||||
NpmPackage npm2 = cache.loadPackage("hl7.fhir.pubpack", "file:" + dir.getAbsolutePath());
|
||||
Assertions.assertNotNull(npm2);
|
||||
Assertions.assertFalse(cache.listPackages().isEmpty());
|
||||
}
|
||||
}
|
|
@ -2,6 +2,8 @@ package org.hl7.fhir.validation;
|
|||
|
||||
import static org.apache.commons.lang3.StringUtils.isBlank;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
/*
|
||||
Copyright (c) 2011+, HL7, Inc.
|
||||
All rights reserved.
|
||||
|
@ -64,18 +66,35 @@ POSSIBILITY OF SUCH DAMAGE.
|
|||
import java.util.List;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.hl7.fhir.r5.context.IWorkerContext;
|
||||
import org.hl7.fhir.r5.elementmodel.Element;
|
||||
import org.hl7.fhir.r5.model.Base;
|
||||
import org.hl7.fhir.r5.model.DomainResource;
|
||||
import org.hl7.fhir.r5.model.Resource;
|
||||
import org.hl7.fhir.r5.model.ValueSet;
|
||||
import org.hl7.fhir.r5.terminologies.ValueSetUtilities;
|
||||
import org.hl7.fhir.utilities.Utilities;
|
||||
import org.hl7.fhir.utilities.i18n.I18nConstants;
|
||||
import org.hl7.fhir.utilities.validation.ValidationMessage;
|
||||
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.validation.instance.utils.IndexedElement;
|
||||
|
||||
public class BaseValidator {
|
||||
|
||||
protected final String META = "meta";
|
||||
protected final String ENTRY = "entry";
|
||||
protected final String DOCUMENT = "document";
|
||||
protected final String RESOURCE = "resource";
|
||||
protected final String MESSAGE = "message";
|
||||
protected final String ID = "id";
|
||||
protected final String FULL_URL = "fullUrl";
|
||||
protected final String PATH_ARG = ":0";
|
||||
protected final String TYPE = "type";
|
||||
protected final String BUNDLE = "Bundle";
|
||||
protected final String LAST_UPDATED = "lastUpdated";
|
||||
|
||||
|
||||
protected Source source;
|
||||
protected IWorkerContext context;
|
||||
protected TimeTracker timeTracker = new TimeTracker();
|
||||
|
@ -616,5 +635,168 @@ public class BaseValidator {
|
|||
return reference;
|
||||
}
|
||||
|
||||
protected Base resolveInBundle(String url, Element bnd) {
|
||||
if (bnd == null)
|
||||
return null;
|
||||
if (bnd.fhirType().equals(BUNDLE)) {
|
||||
for (Element be : bnd.getChildrenByName(ENTRY)) {
|
||||
Element res = be.getNamedChild(RESOURCE);
|
||||
if (res != null) {
|
||||
String fullUrl = be.getChildValue(FULL_URL);
|
||||
String rt = res.fhirType();
|
||||
String id = res.getChildValue(ID);
|
||||
if (url.equals(fullUrl))
|
||||
return res;
|
||||
if (url.equals(rt + "/" + id))
|
||||
return res;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected Element resolveInBundle(List<Element> entries, String ref, String fullUrl, String type, String id) {
|
||||
if (Utilities.isAbsoluteUrl(ref)) {
|
||||
// if the reference is absolute, then you resolve by fullUrl. No other thinking is required.
|
||||
for (Element entry : entries) {
|
||||
String fu = entry.getNamedChildValue(FULL_URL);
|
||||
if (ref.equals(fu))
|
||||
return entry;
|
||||
}
|
||||
return null;
|
||||
} else {
|
||||
// split into base, type, and id
|
||||
String u = null;
|
||||
if (fullUrl != null && fullUrl.endsWith(type + "/" + id))
|
||||
// fullUrl = complex
|
||||
u = fullUrl.substring(0, fullUrl.length() - (type + "/" + id).length()) + ref;
|
||||
// u = fullUrl.substring((type+"/"+id).length())+ref;
|
||||
String[] parts = ref.split("\\/");
|
||||
if (parts.length >= 2) {
|
||||
String t = parts[0];
|
||||
String i = parts[1];
|
||||
for (Element entry : entries) {
|
||||
String fu = entry.getNamedChildValue(FULL_URL);
|
||||
if (fu != null && fu.equals(u))
|
||||
return entry;
|
||||
if (u == null) {
|
||||
Element resource = entry.getNamedChild(RESOURCE);
|
||||
if (resource != null) {
|
||||
String et = resource.getType();
|
||||
String eid = resource.getNamedChildValue(ID);
|
||||
if (t.equals(et) && i.equals(eid))
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
protected IndexedElement getFromBundle(Element bundle, String ref, String fullUrl, List<ValidationMessage> errors, String path, String type, boolean isTransaction) {
|
||||
String targetUrl = null;
|
||||
String version = "";
|
||||
String resourceType = null;
|
||||
if (ref.startsWith("http") || ref.startsWith("urn")) {
|
||||
// We've got an absolute reference, no need to calculate
|
||||
if (ref.contains("/_history/")) {
|
||||
targetUrl = ref.substring(0, ref.indexOf("/_history/") - 1);
|
||||
version = ref.substring(ref.indexOf("/_history/") + 10);
|
||||
} else
|
||||
targetUrl = ref;
|
||||
|
||||
} else if (fullUrl == null) {
|
||||
//This isn't a problem for signatures - if it's a signature, we won't have a resolution for a relative reference. For anything else, this is an error
|
||||
// but this rule doesn't apply for batches or transactions
|
||||
rule(errors, IssueType.REQUIRED, -1, -1, path, Utilities.existsInList(type, "batch-response", "transaction-response") || path.startsWith("Bundle.signature"), I18nConstants.BUNDLE_BUNDLE_FULLURL_MISSING);
|
||||
return null;
|
||||
|
||||
} else if (ref.split("/").length != 2 && ref.split("/").length != 4) {
|
||||
if (isTransaction) {
|
||||
rule(errors, IssueType.INVALID, -1, -1, path, isSearchUrl(ref), I18nConstants.REFERENCE_REF_FORMAT1, ref);
|
||||
} else {
|
||||
rule(errors, IssueType.INVALID, -1, -1, path, false, I18nConstants.REFERENCE_REF_FORMAT2, ref);
|
||||
}
|
||||
return null;
|
||||
|
||||
} else {
|
||||
String base = "";
|
||||
if (fullUrl.startsWith("urn")) {
|
||||
String[] parts = fullUrl.split("\\:");
|
||||
for (int i = 0; i < parts.length - 1; i++) {
|
||||
base = base + parts[i] + ":";
|
||||
}
|
||||
} else {
|
||||
String[] parts;
|
||||
parts = fullUrl.split("/");
|
||||
for (int i = 0; i < parts.length - 2; i++) {
|
||||
base = base + parts[i] + "/";
|
||||
}
|
||||
}
|
||||
|
||||
String id = null;
|
||||
if (ref.contains("/_history/")) {
|
||||
version = ref.substring(ref.indexOf("/_history/") + 10);
|
||||
String[] refBaseParts = ref.substring(0, ref.indexOf("/_history/")).split("/");
|
||||
resourceType = refBaseParts[0];
|
||||
id = refBaseParts[1];
|
||||
} else if (base.startsWith("urn")) {
|
||||
resourceType = ref.split("/")[0];
|
||||
id = ref.split("/")[1];
|
||||
} else
|
||||
id = ref;
|
||||
|
||||
targetUrl = base + id;
|
||||
}
|
||||
|
||||
List<Element> entries = new ArrayList<Element>();
|
||||
bundle.getNamedChildren(ENTRY, entries);
|
||||
Element match = null;
|
||||
int matchIndex = -1;
|
||||
for (int i = 0; i < entries.size(); i++) {
|
||||
Element we = entries.get(i);
|
||||
if (targetUrl.equals(we.getChildValue(FULL_URL))) {
|
||||
Element r = we.getNamedChild(RESOURCE);
|
||||
if (version.isEmpty()) {
|
||||
rule(errors, IssueType.FORBIDDEN, -1, -1, path, match == null, I18nConstants.BUNDLE_BUNDLE_MULTIPLEMATCHES, ref);
|
||||
match = r;
|
||||
matchIndex = i;
|
||||
} else {
|
||||
try {
|
||||
if (version.equals(r.getChildren(META).get(0).getChildValue("versionId"))) {
|
||||
rule(errors, IssueType.FORBIDDEN, -1, -1, path, match == null, I18nConstants.BUNDLE_BUNDLE_MULTIPLEMATCHES, ref);
|
||||
match = r;
|
||||
matchIndex = i;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
warning(errors, IssueType.REQUIRED, -1, -1, path, r.getChildren(META).size() == 1 && r.getChildren(META).get(0).getChildValue("versionId") != null, I18nConstants.BUNDLE_BUNDLE_FULLURL_NEEDVERSION, targetUrl);
|
||||
// If one of these things is null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (match != null && resourceType != null)
|
||||
rule(errors, IssueType.REQUIRED, -1, -1, path, match.getType().equals(resourceType), I18nConstants.REFERENCE_REF_RESOURCETYPE, ref, match.getType());
|
||||
if (match == null)
|
||||
warning(errors, IssueType.REQUIRED, -1, -1, path, !ref.startsWith("urn"), I18nConstants.BUNDLE_BUNDLE_NOT_LOCAL, ref);
|
||||
return match == null ? null : new IndexedElement(matchIndex, match, entries.get(matchIndex));
|
||||
}
|
||||
|
||||
private boolean isSearchUrl(String ref) {
|
||||
if (Utilities.noString(ref) || !ref.contains("?")) {
|
||||
return false;
|
||||
}
|
||||
String tn = ref.substring(0, ref.indexOf("?"));
|
||||
String q = ref.substring(ref.indexOf("?") + 1);
|
||||
if (!context.getResourceNames().contains(tn)) {
|
||||
return false;
|
||||
} else {
|
||||
return q.matches("([_a-zA-Z][_a-zA-Z0-9]*=[^=&]+)(&([_a-zA-Z][_a-zA-Z0-9]*=[^=&]+))*");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -149,6 +149,7 @@ import org.hl7.fhir.utilities.i18n.I18nConstants;
|
|||
import org.hl7.fhir.validation.BaseValidator;
|
||||
import org.hl7.fhir.validation.TimeTracker;
|
||||
import org.hl7.fhir.validation.instance.EnableWhenEvaluator.QStack;
|
||||
import org.hl7.fhir.validation.instance.type.BundleValidator;
|
||||
import org.hl7.fhir.validation.instance.type.CodeSystemValidator;
|
||||
import org.hl7.fhir.validation.instance.type.MeasureValidator;
|
||||
import org.hl7.fhir.validation.instance.type.QuestionnaireValidator;
|
||||
|
@ -188,19 +189,9 @@ import ca.uhn.fhir.util.ObjectUtil;
|
|||
*/
|
||||
|
||||
public class InstanceValidator extends BaseValidator implements IResourceValidator {
|
||||
|
||||
private final String META = "meta";
|
||||
private final String ENTRY = "entry";
|
||||
private final String DOCUMENT = "document";
|
||||
private final String RESOURCE = "resource";
|
||||
private final String MESSAGE = "message";
|
||||
private final String ID = "id";
|
||||
private final String PATH_ARG = ":0";
|
||||
private final String FULL_URL = "fullUrl";
|
||||
private final String TYPE = "type";
|
||||
private final String BUNDLE = "Bundle";
|
||||
private final String LAST_UPDATED = "lastUpdated";
|
||||
|
||||
private static final String EXECUTED_CONSTRAINT_LIST = "validator.executed.invariant.list";
|
||||
private static final String EXECUTION_ID = "validator.execution.id";
|
||||
|
||||
private class ValidatorHostServices implements IEvaluationContext {
|
||||
|
||||
@Override
|
||||
|
@ -278,26 +269,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
|
|||
|
||||
}
|
||||
|
||||
public Base resolveInBundle(String url, Element bnd) {
|
||||
if (bnd == null)
|
||||
return null;
|
||||
if (bnd.fhirType().equals(BUNDLE)) {
|
||||
for (Element be : bnd.getChildrenByName(ENTRY)) {
|
||||
Element res = be.getNamedChild(RESOURCE);
|
||||
if (res != null) {
|
||||
String fullUrl = be.getChildValue(FULL_URL);
|
||||
String rt = res.fhirType();
|
||||
String id = res.getChildValue(ID);
|
||||
if (url.equals(fullUrl))
|
||||
return res;
|
||||
if (url.equals(rt + "/" + id))
|
||||
return res;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean conformsToProfile(Object appContext, Base item, String url) throws FHIRException {
|
||||
ValidatorHostContext ctxt = (ValidatorHostContext) appContext;
|
||||
|
@ -2595,109 +2567,6 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
|
|||
return extensionDomains;
|
||||
}
|
||||
|
||||
private IndexedElement getFromBundle(Element bundle, String ref, String fullUrl, List<ValidationMessage> errors, String path, String type, boolean isTransaction) {
|
||||
String targetUrl = null;
|
||||
String version = "";
|
||||
String resourceType = null;
|
||||
if (ref.startsWith("http") || ref.startsWith("urn")) {
|
||||
// We've got an absolute reference, no need to calculate
|
||||
if (ref.contains("/_history/")) {
|
||||
targetUrl = ref.substring(0, ref.indexOf("/_history/") - 1);
|
||||
version = ref.substring(ref.indexOf("/_history/") + 10);
|
||||
} else
|
||||
targetUrl = ref;
|
||||
|
||||
} else if (fullUrl == null) {
|
||||
//This isn't a problem for signatures - if it's a signature, we won't have a resolution for a relative reference. For anything else, this is an error
|
||||
// but this rule doesn't apply for batches or transactions
|
||||
rule(errors, IssueType.REQUIRED, -1, -1, path, Utilities.existsInList(type, "batch-response", "transaction-response") || path.startsWith("Bundle.signature"), I18nConstants.BUNDLE_BUNDLE_FULLURL_MISSING);
|
||||
return null;
|
||||
|
||||
} else if (ref.split("/").length != 2 && ref.split("/").length != 4) {
|
||||
if (isTransaction) {
|
||||
rule(errors, IssueType.INVALID, -1, -1, path, isSearchUrl(ref), I18nConstants.REFERENCE_REF_FORMAT1, ref);
|
||||
} else {
|
||||
rule(errors, IssueType.INVALID, -1, -1, path, false, I18nConstants.REFERENCE_REF_FORMAT2, ref);
|
||||
}
|
||||
return null;
|
||||
|
||||
} else {
|
||||
String base = "";
|
||||
if (fullUrl.startsWith("urn")) {
|
||||
String[] parts = fullUrl.split("\\:");
|
||||
for (int i = 0; i < parts.length - 1; i++) {
|
||||
base = base + parts[i] + ":";
|
||||
}
|
||||
} else {
|
||||
String[] parts;
|
||||
parts = fullUrl.split("/");
|
||||
for (int i = 0; i < parts.length - 2; i++) {
|
||||
base = base + parts[i] + "/";
|
||||
}
|
||||
}
|
||||
|
||||
String id = null;
|
||||
if (ref.contains("/_history/")) {
|
||||
version = ref.substring(ref.indexOf("/_history/") + 10);
|
||||
String[] refBaseParts = ref.substring(0, ref.indexOf("/_history/")).split("/");
|
||||
resourceType = refBaseParts[0];
|
||||
id = refBaseParts[1];
|
||||
} else if (base.startsWith("urn")) {
|
||||
resourceType = ref.split("/")[0];
|
||||
id = ref.split("/")[1];
|
||||
} else
|
||||
id = ref;
|
||||
|
||||
targetUrl = base + id;
|
||||
}
|
||||
|
||||
List<Element> entries = new ArrayList<Element>();
|
||||
bundle.getNamedChildren(ENTRY, entries);
|
||||
Element match = null;
|
||||
int matchIndex = -1;
|
||||
for (int i = 0; i < entries.size(); i++) {
|
||||
Element we = entries.get(i);
|
||||
if (targetUrl.equals(we.getChildValue(FULL_URL))) {
|
||||
Element r = we.getNamedChild(RESOURCE);
|
||||
if (version.isEmpty()) {
|
||||
rule(errors, IssueType.FORBIDDEN, -1, -1, path, match == null, I18nConstants.BUNDLE_BUNDLE_MULTIPLEMATCHES, ref);
|
||||
match = r;
|
||||
matchIndex = i;
|
||||
} else {
|
||||
try {
|
||||
if (version.equals(r.getChildren(META).get(0).getChildValue("versionId"))) {
|
||||
rule(errors, IssueType.FORBIDDEN, -1, -1, path, match == null, I18nConstants.BUNDLE_BUNDLE_MULTIPLEMATCHES, ref);
|
||||
match = r;
|
||||
matchIndex = i;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
warning(errors, IssueType.REQUIRED, -1, -1, path, r.getChildren(META).size() == 1 && r.getChildren(META).get(0).getChildValue("versionId") != null, I18nConstants.BUNDLE_BUNDLE_FULLURL_NEEDVERSION, targetUrl);
|
||||
// If one of these things is null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (match != null && resourceType != null)
|
||||
rule(errors, IssueType.REQUIRED, -1, -1, path, match.getType().equals(resourceType), I18nConstants.REFERENCE_REF_RESOURCETYPE, ref, match.getType());
|
||||
if (match == null)
|
||||
warning(errors, IssueType.REQUIRED, -1, -1, path, !ref.startsWith("urn"), I18nConstants.BUNDLE_BUNDLE_NOT_LOCAL, ref);
|
||||
return match == null ? null : new IndexedElement(matchIndex, match, entries.get(matchIndex));
|
||||
}
|
||||
|
||||
private boolean isSearchUrl(String ref) {
|
||||
if (Utilities.noString(ref) || !ref.contains("?")) {
|
||||
return false;
|
||||
}
|
||||
String tn = ref.substring(0, ref.indexOf("?"));
|
||||
String q = ref.substring(ref.indexOf("?") + 1);
|
||||
if (!context.getResourceNames().contains(tn)) {
|
||||
return false;
|
||||
} else {
|
||||
return q.matches("([_a-zA-Z][_a-zA-Z0-9]*=[^=&]+)(&([_a-zA-Z][_a-zA-Z0-9]*=[^=&]+))*");
|
||||
}
|
||||
}
|
||||
|
||||
private StructureDefinition getProfileForType(String type, List<TypeRefComponent> list) {
|
||||
for (TypeRefComponent tr : list) {
|
||||
String url = tr.getWorkingCode();
|
||||
|
@ -2964,45 +2833,6 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
|
|||
}
|
||||
|
||||
|
||||
private Element resolveInBundle(List<Element> entries, String ref, String fullUrl, String type, String id) {
|
||||
if (Utilities.isAbsoluteUrl(ref)) {
|
||||
// if the reference is absolute, then you resolve by fullUrl. No other thinking is required.
|
||||
for (Element entry : entries) {
|
||||
String fu = entry.getNamedChildValue(FULL_URL);
|
||||
if (ref.equals(fu))
|
||||
return entry;
|
||||
}
|
||||
return null;
|
||||
} else {
|
||||
// split into base, type, and id
|
||||
String u = null;
|
||||
if (fullUrl != null && fullUrl.endsWith(type + "/" + id))
|
||||
// fullUrl = complex
|
||||
u = fullUrl.substring(0, fullUrl.length() - (type + "/" + id).length()) + ref;
|
||||
// u = fullUrl.substring((type+"/"+id).length())+ref;
|
||||
String[] parts = ref.split("\\/");
|
||||
if (parts.length >= 2) {
|
||||
String t = parts[0];
|
||||
String i = parts[1];
|
||||
for (Element entry : entries) {
|
||||
String fu = entry.getNamedChildValue(FULL_URL);
|
||||
if (fu != null && fu.equals(u))
|
||||
return entry;
|
||||
if (u == null) {
|
||||
Element resource = entry.getNamedChild(RESOURCE);
|
||||
if (resource != null) {
|
||||
String et = resource.getType();
|
||||
String eid = resource.getNamedChildValue(ID);
|
||||
if (t.equals(et) && i.equals(eid))
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private ElementDefinition resolveNameReference(StructureDefinitionSnapshotComponent snapshot, String contentReference) {
|
||||
for (ElementDefinition ed : snapshot.getElement())
|
||||
if (contentReference.equals("#" + ed.getId()))
|
||||
|
@ -3486,7 +3316,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
|
|||
public void checkSpecials(ValidatorHostContext hostContext, List<ValidationMessage> errors, Element element, NodeStack stack, boolean checkSpecials) {
|
||||
// specific known special validations
|
||||
if (element.getType().equals(BUNDLE)) {
|
||||
validateBundle(errors, element, stack, checkSpecials);
|
||||
new BundleValidator(context, serverBase).validateBundle(errors, element, stack, checkSpecials);
|
||||
} else if (element.getType().equals("Observation")) {
|
||||
validateObservation(errors, element, stack);
|
||||
} else if (element.getType().equals("Questionnaire")) {
|
||||
|
@ -3583,268 +3413,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
|
|||
iRest++;
|
||||
}
|
||||
}
|
||||
|
||||
private void validateBundle(List<ValidationMessage> errors, Element bundle, NodeStack stack, boolean checkSpecials) {
|
||||
List<Element> entries = new ArrayList<Element>();
|
||||
bundle.getNamedChildren(ENTRY, entries);
|
||||
String type = bundle.getNamedChildValue(TYPE);
|
||||
type = StringUtils.defaultString(type);
|
||||
|
||||
if (entries.size() == 0) {
|
||||
rule(errors, IssueType.INVALID, stack.getLiteralPath(), !(type.equals(DOCUMENT) || type.equals(MESSAGE)), I18nConstants.BUNDLE_BUNDLE_ENTRY_NOFIRST);
|
||||
} else {
|
||||
// Get the first entry, the MessageHeader
|
||||
Element firstEntry = entries.get(0);
|
||||
// Get the stack of the first entry
|
||||
NodeStack firstStack = stack.push(firstEntry, 1, null, null);
|
||||
|
||||
String fullUrl = firstEntry.getNamedChildValue(FULL_URL);
|
||||
|
||||
if (type.equals(DOCUMENT)) {
|
||||
Element resource = firstEntry.getNamedChild(RESOURCE);
|
||||
String id = resource.getNamedChildValue(ID);
|
||||
if (rule(errors, IssueType.INVALID, firstEntry.line(), firstEntry.col(), stack.addToLiteralPath(ENTRY, PATH_ARG), resource != null, I18nConstants.BUNDLE_BUNDLE_ENTRY_NOFIRSTRESOURCE)) {
|
||||
validateDocument(errors, entries, resource, firstStack.push(resource, -1, null, null), fullUrl, id);
|
||||
}
|
||||
if (!VersionUtilities.isThisOrLater(FHIRVersion._4_0_1.getDisplay(), bundle.getProperty().getStructure().getFhirVersion().getDisplay())) {
|
||||
handleSpecialCaseForLastUpdated(bundle, errors, stack);
|
||||
}
|
||||
checkAllInterlinked(errors, entries, stack, bundle, true);
|
||||
}
|
||||
if (type.equals(MESSAGE)) {
|
||||
Element resource = firstEntry.getNamedChild(RESOURCE);
|
||||
String id = resource.getNamedChildValue(ID);
|
||||
if (rule(errors, IssueType.INVALID, firstEntry.line(), firstEntry.col(), stack.addToLiteralPath(ENTRY, PATH_ARG), resource != null, I18nConstants.BUNDLE_BUNDLE_ENTRY_NOFIRSTRESOURCE)) {
|
||||
validateMessage(errors, entries, resource, firstStack.push(resource, -1, null, null), fullUrl, id);
|
||||
}
|
||||
checkAllInterlinked(errors, entries, stack, bundle, VersionUtilities.isR5Ver(context.getVersion()));
|
||||
}
|
||||
// We do not yet have rules requiring that the id and fullUrl match when dealing with messaging Bundles
|
||||
// validateResourceIds(errors, entries, stack);
|
||||
}
|
||||
for (Element entry : entries) {
|
||||
String fullUrl = entry.getNamedChildValue(FULL_URL);
|
||||
String url = getCanonicalURLForEntry(entry);
|
||||
String id = getIdForEntry(entry);
|
||||
if (url != null) {
|
||||
if (!(!url.equals(fullUrl) || (url.matches(uriRegexForVersion()) && url.endsWith("/" + id))) && !isV3orV2Url(url))
|
||||
rule(errors, IssueType.INVALID, entry.line(), entry.col(), stack.addToLiteralPath(ENTRY, PATH_ARG), false, I18nConstants.BUNDLE_BUNDLE_ENTRY_MISMATCHIDURL, url, fullUrl, id);
|
||||
rule(errors, IssueType.INVALID, entry.line(), entry.col(), stack.addToLiteralPath(ENTRY, PATH_ARG), !url.equals(fullUrl) || serverBase == null || (url.equals(Utilities.pathURL(serverBase, entry.getNamedChild(RESOURCE).fhirType(), id))), I18nConstants.BUNDLE_BUNDLE_ENTRY_CANONICAL, url, fullUrl);
|
||||
}
|
||||
// todo: check specials
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* As per outline for <a href=http://hl7.org/fhir/stu3/documents.html#content>Document Content</a>:
|
||||
* <li>"The document date (mandatory). This is found in Bundle.meta.lastUpdated and identifies when the document bundle
|
||||
* was assembled from the underlying resources"</li>
|
||||
* <p></p>
|
||||
* This check was not being done for release versions < r4.
|
||||
* <p></p>
|
||||
* Related JIRA ticket is <a href=https://jira.hl7.org/browse/FHIR-26544>FHIR-26544</a>
|
||||
*
|
||||
* @param bundle {@link org.hl7.fhir.r5.elementmodel}
|
||||
* @param errors {@link List<ValidationMessage>}
|
||||
* @param stack {@link NodeStack}
|
||||
*/
|
||||
private void handleSpecialCaseForLastUpdated(Element bundle, List<ValidationMessage> errors, NodeStack stack) {
|
||||
boolean ok = bundle.hasChild(META)
|
||||
&& bundle.getNamedChild(META).hasChild(LAST_UPDATED)
|
||||
&& bundle.getNamedChild(META).getNamedChild(LAST_UPDATED).hasValue();
|
||||
rule(errors, IssueType.REQUIRED, stack.getLiteralPath(), ok, I18nConstants.DOCUMENT_DATE_REQUIRED, I18nConstants.DOCUMENT_DATE_REQUIRED_HTML);
|
||||
}
|
||||
|
||||
|
||||
// hack for pre-UTG v2/v3
|
||||
private boolean isV3orV2Url(String url) {
|
||||
return url.startsWith("http://hl7.org/fhir/v3/") || url.startsWith("http://hl7.org/fhir/v2/");
|
||||
}
|
||||
|
||||
public final static String URI_REGEX3 = "((http|https)://([A-Za-z0-9\\\\\\.\\:\\%\\$]*\\/)*)?(Account|ActivityDefinition|AllergyIntolerance|AdverseEvent|Appointment|AppointmentResponse|AuditEvent|Basic|Binary|BodySite|Bundle|CapabilityStatement|CarePlan|CareTeam|ChargeItem|Claim|ClaimResponse|ClinicalImpression|CodeSystem|Communication|CommunicationRequest|CompartmentDefinition|Composition|ConceptMap|Condition (aka Problem)|Consent|Contract|Coverage|DataElement|DetectedIssue|Device|DeviceComponent|DeviceMetric|DeviceRequest|DeviceUseStatement|DiagnosticReport|DocumentManifest|DocumentReference|EligibilityRequest|EligibilityResponse|Encounter|Endpoint|EnrollmentRequest|EnrollmentResponse|EpisodeOfCare|ExpansionProfile|ExplanationOfBenefit|FamilyMemberHistory|Flag|Goal|GraphDefinition|Group|GuidanceResponse|HealthcareService|ImagingManifest|ImagingStudy|Immunization|ImmunizationRecommendation|ImplementationGuide|Library|Linkage|List|Location|Measure|MeasureReport|Media|Medication|MedicationAdministration|MedicationDispense|MedicationRequest|MedicationStatement|MessageDefinition|MessageHeader|NamingSystem|NutritionOrder|Observation|OperationDefinition|OperationOutcome|Organization|Parameters|Patient|PaymentNotice|PaymentReconciliation|Person|PlanDefinition|Practitioner|PractitionerRole|Procedure|ProcedureRequest|ProcessRequest|ProcessResponse|Provenance|Questionnaire|QuestionnaireResponse|ReferralRequest|RelatedPerson|RequestGroup|ResearchStudy|ResearchSubject|RiskAssessment|Schedule|SearchParameter|Sequence|ServiceDefinition|Slot|Specimen|StructureDefinition|StructureMap|Subscription|Substance|SupplyDelivery|SupplyRequest|Task|TestScript|TestReport|ValueSet|VisionPrescription)\\/[A-Za-z0-9\\-\\.]{1,64}(\\/_history\\/[A-Za-z0-9\\-\\.]{1,64})?";
|
||||
private static final String EXECUTED_CONSTRAINT_LIST = "validator.executed.invariant.list";
|
||||
private static final String EXECUTION_ID = "validator.execution.id";
|
||||
|
||||
private String uriRegexForVersion() {
|
||||
if (VersionUtilities.isR3Ver(context.getVersion()))
|
||||
return URI_REGEX3;
|
||||
else
|
||||
return Constants.URI_REGEX;
|
||||
}
|
||||
|
||||
private String getCanonicalURLForEntry(Element entry) {
|
||||
Element e = entry.getNamedChild(RESOURCE);
|
||||
if (e == null)
|
||||
return null;
|
||||
return e.getNamedChildValue("url");
|
||||
}
|
||||
|
||||
private String getIdForEntry(Element entry) {
|
||||
Element e = entry.getNamedChild(RESOURCE);
|
||||
if (e == null)
|
||||
return null;
|
||||
return e.getNamedChildValue(ID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check each resource entry to ensure that the entry's fullURL includes the resource's id
|
||||
* value. Adds an ERROR ValidationMessge to errors List for a given entry if it references
|
||||
* a resource and fullURL does not include the resource's id.
|
||||
*
|
||||
* @param errors List of ValidationMessage objects that new errors will be added to.
|
||||
* @param entries List of entry Element objects to be checked.
|
||||
* @param stack Current NodeStack used to create path names in error detail messages.
|
||||
*/
|
||||
private void validateResourceIds(List<ValidationMessage> errors, List<Element> entries, NodeStack stack) {
|
||||
// TODO: Need to handle _version
|
||||
int i = 1;
|
||||
for (Element entry : entries) {
|
||||
String fullUrl = entry.getNamedChildValue(FULL_URL);
|
||||
Element resource = entry.getNamedChild(RESOURCE);
|
||||
String id = resource != null ? resource.getNamedChildValue(ID) : null;
|
||||
if (id != null && fullUrl != null) {
|
||||
String urlId = null;
|
||||
if (fullUrl.startsWith("https://") || fullUrl.startsWith("http://")) {
|
||||
urlId = fullUrl.substring(fullUrl.lastIndexOf('/') + 1);
|
||||
} else if (fullUrl.startsWith("urn:uuid") || fullUrl.startsWith("urn:oid")) {
|
||||
urlId = fullUrl.substring(fullUrl.lastIndexOf(':') + 1);
|
||||
}
|
||||
rule(errors, IssueType.INVALID, entry.line(), entry.col(), stack.addToLiteralPath("entry[" + i + "]"), urlId.equals(id), I18nConstants.BUNDLE_BUNDLE_ENTRY_IDURLMISMATCH, id, fullUrl);
|
||||
}
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
private void checkAllInterlinked(List<ValidationMessage> errors, List<Element> entries, NodeStack stack, Element bundle, boolean isError) {
|
||||
List<EntrySummary> entryList = new ArrayList<>();
|
||||
for (Element entry : entries) {
|
||||
Element r = entry.getNamedChild(RESOURCE);
|
||||
if (r != null) {
|
||||
entryList.add(new EntrySummary(entry, r));
|
||||
}
|
||||
}
|
||||
for (EntrySummary e : entryList) {
|
||||
Set<String> references = findReferences(e.getEntry());
|
||||
for (String ref : references) {
|
||||
Element tgt = resolveInBundle(entries, ref, e.getEntry().getChildValue(FULL_URL), e.getResource().fhirType(), e.getResource().getIdBase());
|
||||
if (tgt != null) {
|
||||
EntrySummary t = entryForTarget(entryList, tgt);
|
||||
if (t != null) {
|
||||
e.getTargets().add(t);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Set<EntrySummary> visited = new HashSet<>();
|
||||
visitLinked(visited, entryList.get(0));
|
||||
boolean foundRevLinks;
|
||||
do {
|
||||
foundRevLinks = false;
|
||||
for (EntrySummary e : entryList) {
|
||||
if (!visited.contains(e)) {
|
||||
boolean add = false;
|
||||
for (EntrySummary t : e.getTargets()) {
|
||||
if (visited.contains(t)) {
|
||||
add = true;
|
||||
}
|
||||
}
|
||||
if (add) {
|
||||
foundRevLinks = true;
|
||||
visitLinked(visited, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
} while (foundRevLinks);
|
||||
|
||||
int i = 0;
|
||||
for (EntrySummary e : entryList) {
|
||||
Element entry = e.getEntry();
|
||||
if (isError) {
|
||||
rule(errors, IssueType.INFORMATIONAL, entry.line(), entry.col(), stack.addToLiteralPath(ENTRY + '[' + (i + 1) + ']'), visited.contains(e), I18nConstants.BUNDLE_BUNDLE_ENTRY_ORPHAN, (entry.getChildValue(FULL_URL) != null ? "'" + entry.getChildValue(FULL_URL) + "'" : ""));
|
||||
} else {
|
||||
warning(errors, IssueType.INFORMATIONAL, entry.line(), entry.col(), stack.addToLiteralPath(ENTRY + '[' + (i + 1) + ']'), visited.contains(e), I18nConstants.BUNDLE_BUNDLE_ENTRY_ORPHAN, (entry.getChildValue(FULL_URL) != null ? "'" + entry.getChildValue(FULL_URL) + "'" : ""));
|
||||
}
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
private EntrySummary entryForTarget(List<EntrySummary> entryList, Element tgt) {
|
||||
for (EntrySummary e : entryList) {
|
||||
if (e.getEntry() == tgt) {
|
||||
return e;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void visitLinked(Set<EntrySummary> visited, EntrySummary t) {
|
||||
if (!visited.contains(t)) {
|
||||
visited.add(t);
|
||||
for (EntrySummary e : t.getTargets()) {
|
||||
visitLinked(visited, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void followResourceLinks(Element entry, Map<String, Element> visitedResources, Map<Element, Element> candidateEntries, List<Element> candidateResources, List<ValidationMessage> errors, NodeStack stack) {
|
||||
followResourceLinks(entry, visitedResources, candidateEntries, candidateResources, errors, stack, 0);
|
||||
}
|
||||
|
||||
private void followResourceLinks(Element entry, Map<String, Element> visitedResources, Map<Element, Element> candidateEntries, List<Element> candidateResources, List<ValidationMessage> errors, NodeStack stack, int depth) {
|
||||
Element resource = entry.getNamedChild(RESOURCE);
|
||||
if (visitedResources.containsValue(resource))
|
||||
return;
|
||||
|
||||
visitedResources.put(entry.getNamedChildValue(FULL_URL), resource);
|
||||
|
||||
String type = null;
|
||||
Set<String> references = findReferences(resource);
|
||||
for (String reference : references) {
|
||||
// We don't want errors when just retrieving the element as they will be caught (with better path info) in subsequent processing
|
||||
IndexedElement r = getFromBundle(stack.getElement(), reference, entry.getChildValue(FULL_URL), new ArrayList<ValidationMessage>(), stack.addToLiteralPath("entry[" + candidateResources.indexOf(resource) + "]"), type, "transaction".equals(stack.getElement().getChildValue(TYPE)));
|
||||
if (r != null && !visitedResources.containsValue(r.getMatch())) {
|
||||
followResourceLinks(candidateEntries.get(r.getMatch()), visitedResources, candidateEntries, candidateResources, errors, stack, depth + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Set<String> findReferences(Element start) {
|
||||
Set<String> references = new HashSet<String>();
|
||||
findReferences(start, references);
|
||||
return references;
|
||||
}
|
||||
|
||||
private void findReferences(Element start, Set<String> references) {
|
||||
for (Element child : start.getChildren()) {
|
||||
if (child.getType().equals("Reference")) {
|
||||
String ref = child.getChildValue("reference");
|
||||
if (ref != null && !ref.startsWith("#"))
|
||||
references.add(ref);
|
||||
}
|
||||
if (child.getType().equals("url") || child.getType().equals("uri") || child.getType().equals("canonical")) {
|
||||
String ref = child.primitiveValue();
|
||||
if (ref != null && !ref.startsWith("#"))
|
||||
references.add(ref);
|
||||
}
|
||||
findReferences(child, references);
|
||||
}
|
||||
}
|
||||
|
||||
private void validateBundleReference(List<ValidationMessage> errors, List<Element> entries, Element ref, String name, NodeStack stack, String fullUrl, String type, String id) {
|
||||
String reference = null;
|
||||
try {
|
||||
reference = ref.getNamedChildValue("reference");
|
||||
} catch (Error e) {
|
||||
|
||||
}
|
||||
|
||||
if (ref != null && !Utilities.noString(reference) && !reference.startsWith("#")) {
|
||||
Element target = resolveInBundle(entries, reference, fullUrl, type, id);
|
||||
rule(errors, IssueType.INVALID, ref.line(), ref.col(), stack.addToLiteralPath("reference"), target != null, I18nConstants.BUNDLE_BUNDLE_ENTRY_NOTFOUND, reference, name);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void validateContains(ValidatorHostContext hostContext, List<ValidationMessage> errors, String path, ElementDefinition child, ElementDefinition context, Element resource, Element element, NodeStack stack, IdStatus idstatus) throws FHIRException {
|
||||
String resourceName = element.getType();
|
||||
TypeRefComponent trr = null;
|
||||
|
@ -3919,50 +3488,6 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
|
|||
return false;
|
||||
}
|
||||
|
||||
private void validateDocument(List<ValidationMessage> errors, List<Element> entries, Element composition, NodeStack stack, String fullUrl, String id) {
|
||||
// first entry must be a composition
|
||||
if (rule(errors, IssueType.INVALID, composition.line(), composition.col(), stack.getLiteralPath(), composition.getType().equals("Composition"), I18nConstants.BUNDLE_BUNDLE_ENTRY_DOCUMENT)) {
|
||||
|
||||
// the composition subject etc references must resolve in the bundle
|
||||
validateDocumentReference(errors, entries, composition, stack, fullUrl, id, false, "subject", "Composition");
|
||||
validateDocumentReference(errors, entries, composition, stack, fullUrl, id, true, "author", "Composition");
|
||||
validateDocumentReference(errors, entries, composition, stack, fullUrl, id, false, "encounter", "Composition");
|
||||
validateDocumentReference(errors, entries, composition, stack, fullUrl, id, false, "custodian", "Composition");
|
||||
validateDocumentSubReference(errors, entries, composition, stack, fullUrl, id, "Composition", "attester", false, "party");
|
||||
validateDocumentSubReference(errors, entries, composition, stack, fullUrl, id, "Composition", "event", true, "detail");
|
||||
|
||||
validateSections(errors, entries, composition, stack, fullUrl, id);
|
||||
}
|
||||
}
|
||||
|
||||
public void validateDocumentSubReference(List<ValidationMessage> errors, List<Element> entries, Element composition, NodeStack stack, String fullUrl, String id, String title, String parent, boolean repeats, String propName) {
|
||||
List<Element> list = new ArrayList<>();
|
||||
composition.getNamedChildren(parent, list);
|
||||
int i = 1;
|
||||
for (Element elem : list) {
|
||||
validateDocumentReference(errors, entries, elem, stack.push(elem, i, null, null), fullUrl, id, repeats, propName, title + "." + parent);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
public void validateDocumentReference(List<ValidationMessage> errors, List<Element> entries, Element composition, NodeStack stack, String fullUrl, String id, boolean repeats, String propName, String title) {
|
||||
if (repeats) {
|
||||
List<Element> list = new ArrayList<>();
|
||||
composition.getNamedChildren(propName, list);
|
||||
int i = 1;
|
||||
for (Element elem : list) {
|
||||
|
||||
validateBundleReference(errors, entries, elem, title + "." + propName, stack.push(elem, i, null, null), fullUrl, "Composition", id);
|
||||
i++;
|
||||
}
|
||||
|
||||
} else {
|
||||
Element elem = composition.getNamedChild(propName);
|
||||
if (elem != null) {
|
||||
validateBundleReference(errors, entries, elem, title + "." + propName, stack.push(elem, -1, null, null), fullUrl, "Composition", id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void validateElement(ValidatorHostContext hostContext, List<ValidationMessage> errors, StructureDefinition profile, ElementDefinition definition, StructureDefinition cprofile, ElementDefinition context,
|
||||
Element resource, Element element, String actualType, NodeStack stack, boolean inCodeableConcept, boolean checkDisplayInContext, String extensionUrl) throws FHIRException {
|
||||
|
@ -4668,16 +4193,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void validateMessage(List<ValidationMessage> errors, List<Element> entries, Element messageHeader, NodeStack stack, String fullUrl, String id) {
|
||||
// first entry must be a messageheader
|
||||
if (rule(errors, IssueType.INVALID, messageHeader.line(), messageHeader.col(), stack.getLiteralPath(), messageHeader.getType().equals("MessageHeader"), I18nConstants.VALIDATION_BUNDLE_MESSAGE)) {
|
||||
List<Element> elements = messageHeader.getChildren("focus");
|
||||
for (Element elem : elements)
|
||||
validateBundleReference(errors, entries, elem, "MessageHeader Data", stack.push(elem, -1, null, null), fullUrl, "MessageHeader", id);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void validateObservation(List<ValidationMessage> errors, Element element, NodeStack stack) {
|
||||
// all observations should have a subject, a performer, and a time
|
||||
|
||||
|
@ -4742,30 +4258,6 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
|
|||
}
|
||||
}
|
||||
|
||||
private void validateSections(List<ValidationMessage> errors, List<Element> entries, Element focus, NodeStack stack, String fullUrl, String id) {
|
||||
List<Element> sections = new ArrayList<Element>();
|
||||
focus.getNamedChildren("section", sections);
|
||||
int i = 1;
|
||||
for (Element section : sections) {
|
||||
NodeStack localStack = stack.push(section, i, null, null);
|
||||
|
||||
// technically R4+, but there won't be matches from before that
|
||||
validateDocumentReference(errors, entries, section, stack, fullUrl, id, false, "author", "Section");
|
||||
validateDocumentReference(errors, entries, section, stack, fullUrl, id, false, "focus", "Section");
|
||||
|
||||
List<Element> sectionEntries = new ArrayList<Element>();
|
||||
section.getNamedChildren(ENTRY, sectionEntries);
|
||||
int j = 1;
|
||||
for (Element sectionEntry : sectionEntries) {
|
||||
NodeStack localStack2 = localStack.push(sectionEntry, j, null, null);
|
||||
validateBundleReference(errors, entries, sectionEntry, "Section Entry", localStack2, fullUrl, "Composition", id);
|
||||
j++;
|
||||
}
|
||||
validateSections(errors, entries, section, localStack, fullUrl, id);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean valueMatchesCriteria(Element value, ElementDefinition criteria, StructureDefinition profile) throws FHIRException {
|
||||
if (criteria.hasFixed()) {
|
||||
List<ValidationMessage> msgs = new ArrayList<ValidationMessage>();
|
||||
|
|
|
@ -0,0 +1,375 @@
|
|||
package org.hl7.fhir.validation.instance.type;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.hl7.fhir.r5.context.IWorkerContext;
|
||||
import org.hl7.fhir.r5.elementmodel.Element;
|
||||
import org.hl7.fhir.r5.model.Constants;
|
||||
import org.hl7.fhir.r5.model.Enumerations.FHIRVersion;
|
||||
import org.hl7.fhir.utilities.Utilities;
|
||||
import org.hl7.fhir.utilities.VersionUtilities;
|
||||
import org.hl7.fhir.utilities.i18n.I18nConstants;
|
||||
import org.hl7.fhir.utilities.validation.ValidationMessage;
|
||||
import org.hl7.fhir.utilities.validation.ValidationMessage.IssueType;
|
||||
import org.hl7.fhir.validation.BaseValidator;
|
||||
import org.hl7.fhir.validation.instance.utils.EntrySummary;
|
||||
import org.hl7.fhir.validation.instance.utils.IndexedElement;
|
||||
import org.hl7.fhir.validation.instance.utils.NodeStack;
|
||||
|
||||
public class BundleValidator extends BaseValidator{
|
||||
public final static String URI_REGEX3 = "((http|https)://([A-Za-z0-9\\\\\\.\\:\\%\\$]*\\/)*)?(Account|ActivityDefinition|AllergyIntolerance|AdverseEvent|Appointment|AppointmentResponse|AuditEvent|Basic|Binary|BodySite|Bundle|CapabilityStatement|CarePlan|CareTeam|ChargeItem|Claim|ClaimResponse|ClinicalImpression|CodeSystem|Communication|CommunicationRequest|CompartmentDefinition|Composition|ConceptMap|Condition (aka Problem)|Consent|Contract|Coverage|DataElement|DetectedIssue|Device|DeviceComponent|DeviceMetric|DeviceRequest|DeviceUseStatement|DiagnosticReport|DocumentManifest|DocumentReference|EligibilityRequest|EligibilityResponse|Encounter|Endpoint|EnrollmentRequest|EnrollmentResponse|EpisodeOfCare|ExpansionProfile|ExplanationOfBenefit|FamilyMemberHistory|Flag|Goal|GraphDefinition|Group|GuidanceResponse|HealthcareService|ImagingManifest|ImagingStudy|Immunization|ImmunizationRecommendation|ImplementationGuide|Library|Linkage|List|Location|Measure|MeasureReport|Media|Medication|MedicationAdministration|MedicationDispense|MedicationRequest|MedicationStatement|MessageDefinition|MessageHeader|NamingSystem|NutritionOrder|Observation|OperationDefinition|OperationOutcome|Organization|Parameters|Patient|PaymentNotice|PaymentReconciliation|Person|PlanDefinition|Practitioner|PractitionerRole|Procedure|ProcedureRequest|ProcessRequest|ProcessResponse|Provenance|Questionnaire|QuestionnaireResponse|ReferralRequest|RelatedPerson|RequestGroup|ResearchStudy|ResearchSubject|RiskAssessment|Schedule|SearchParameter|Sequence|ServiceDefinition|Slot|Specimen|StructureDefinition|StructureMap|Subscription|Substance|SupplyDelivery|SupplyRequest|Task|TestScript|TestReport|ValueSet|VisionPrescription)\\/[A-Za-z0-9\\-\\.]{1,64}(\\/_history\\/[A-Za-z0-9\\-\\.]{1,64})?";
|
||||
private String serverBase;
|
||||
|
||||
public BundleValidator(IWorkerContext context, String serverBase) {
|
||||
super(context);
|
||||
this.serverBase = serverBase;
|
||||
}
|
||||
|
||||
public void validateBundle(List<ValidationMessage> errors, Element bundle, NodeStack stack, boolean checkSpecials) {
|
||||
List<Element> entries = new ArrayList<Element>();
|
||||
bundle.getNamedChildren(ENTRY, entries);
|
||||
String type = bundle.getNamedChildValue(TYPE);
|
||||
type = StringUtils.defaultString(type);
|
||||
|
||||
if (entries.size() == 0) {
|
||||
rule(errors, IssueType.INVALID, stack.getLiteralPath(), !(type.equals(DOCUMENT) || type.equals(MESSAGE)), I18nConstants.BUNDLE_BUNDLE_ENTRY_NOFIRST);
|
||||
} else {
|
||||
// Get the first entry, the MessageHeader
|
||||
Element firstEntry = entries.get(0);
|
||||
// Get the stack of the first entry
|
||||
NodeStack firstStack = stack.push(firstEntry, 1, null, null);
|
||||
|
||||
String fullUrl = firstEntry.getNamedChildValue(FULL_URL);
|
||||
|
||||
if (type.equals(DOCUMENT)) {
|
||||
Element resource = firstEntry.getNamedChild(RESOURCE);
|
||||
if (rule(errors, IssueType.INVALID, firstEntry.line(), firstEntry.col(), stack.addToLiteralPath(ENTRY, PATH_ARG), resource != null, I18nConstants.BUNDLE_BUNDLE_ENTRY_NOFIRSTRESOURCE)) {
|
||||
String id = resource.getNamedChildValue(ID);
|
||||
validateDocument(errors, entries, resource, firstStack.push(resource, -1, null, null), fullUrl, id);
|
||||
}
|
||||
if (!VersionUtilities.isThisOrLater(FHIRVersion._4_0_1.getDisplay(), bundle.getProperty().getStructure().getFhirVersion().getDisplay())) {
|
||||
handleSpecialCaseForLastUpdated(bundle, errors, stack);
|
||||
}
|
||||
checkAllInterlinked(errors, entries, stack, bundle, true);
|
||||
}
|
||||
if (type.equals(MESSAGE)) {
|
||||
Element resource = firstEntry.getNamedChild(RESOURCE);
|
||||
String id = resource.getNamedChildValue(ID);
|
||||
if (rule(errors, IssueType.INVALID, firstEntry.line(), firstEntry.col(), stack.addToLiteralPath(ENTRY, PATH_ARG), resource != null, I18nConstants.BUNDLE_BUNDLE_ENTRY_NOFIRSTRESOURCE)) {
|
||||
validateMessage(errors, entries, resource, firstStack.push(resource, -1, null, null), fullUrl, id);
|
||||
}
|
||||
checkAllInterlinked(errors, entries, stack, bundle, VersionUtilities.isR5Ver(context.getVersion()));
|
||||
}
|
||||
// We do not yet have rules requiring that the id and fullUrl match when dealing with messaging Bundles
|
||||
// validateResourceIds(errors, entries, stack);
|
||||
}
|
||||
for (Element entry : entries) {
|
||||
String fullUrl = entry.getNamedChildValue(FULL_URL);
|
||||
String url = getCanonicalURLForEntry(entry);
|
||||
String id = getIdForEntry(entry);
|
||||
if (url != null) {
|
||||
if (!(!url.equals(fullUrl) || (url.matches(uriRegexForVersion()) && url.endsWith("/" + id))) && !isV3orV2Url(url))
|
||||
rule(errors, IssueType.INVALID, entry.line(), entry.col(), stack.addToLiteralPath(ENTRY, PATH_ARG), false, I18nConstants.BUNDLE_BUNDLE_ENTRY_MISMATCHIDURL, url, fullUrl, id);
|
||||
rule(errors, IssueType.INVALID, entry.line(), entry.col(), stack.addToLiteralPath(ENTRY, PATH_ARG), !url.equals(fullUrl) || serverBase == null || (url.equals(Utilities.pathURL(serverBase, entry.getNamedChild(RESOURCE).fhirType(), id))), I18nConstants.BUNDLE_BUNDLE_ENTRY_CANONICAL, url, fullUrl);
|
||||
}
|
||||
// todo: check specials
|
||||
}
|
||||
}
|
||||
|
||||
private void validateDocument(List<ValidationMessage> errors, List<Element> entries, Element composition, NodeStack stack, String fullUrl, String id) {
|
||||
// first entry must be a composition
|
||||
if (rule(errors, IssueType.INVALID, composition.line(), composition.col(), stack.getLiteralPath(), composition.getType().equals("Composition"), I18nConstants.BUNDLE_BUNDLE_ENTRY_DOCUMENT)) {
|
||||
|
||||
// the composition subject etc references must resolve in the bundle
|
||||
validateDocumentReference(errors, entries, composition, stack, fullUrl, id, false, "subject", "Composition");
|
||||
validateDocumentReference(errors, entries, composition, stack, fullUrl, id, true, "author", "Composition");
|
||||
validateDocumentReference(errors, entries, composition, stack, fullUrl, id, false, "encounter", "Composition");
|
||||
validateDocumentReference(errors, entries, composition, stack, fullUrl, id, false, "custodian", "Composition");
|
||||
validateDocumentSubReference(errors, entries, composition, stack, fullUrl, id, "Composition", "attester", false, "party");
|
||||
validateDocumentSubReference(errors, entries, composition, stack, fullUrl, id, "Composition", "event", true, "detail");
|
||||
|
||||
validateSections(errors, entries, composition, stack, fullUrl, id);
|
||||
}
|
||||
}
|
||||
|
||||
private void validateSections(List<ValidationMessage> errors, List<Element> entries, Element focus, NodeStack stack, String fullUrl, String id) {
|
||||
List<Element> sections = new ArrayList<Element>();
|
||||
focus.getNamedChildren("section", sections);
|
||||
int i = 1;
|
||||
for (Element section : sections) {
|
||||
NodeStack localStack = stack.push(section, i, null, null);
|
||||
|
||||
// technically R4+, but there won't be matches from before that
|
||||
validateDocumentReference(errors, entries, section, stack, fullUrl, id, false, "author", "Section");
|
||||
validateDocumentReference(errors, entries, section, stack, fullUrl, id, false, "focus", "Section");
|
||||
|
||||
List<Element> sectionEntries = new ArrayList<Element>();
|
||||
section.getNamedChildren(ENTRY, sectionEntries);
|
||||
int j = 1;
|
||||
for (Element sectionEntry : sectionEntries) {
|
||||
NodeStack localStack2 = localStack.push(sectionEntry, j, null, null);
|
||||
validateBundleReference(errors, entries, sectionEntry, "Section Entry", localStack2, fullUrl, "Composition", id);
|
||||
j++;
|
||||
}
|
||||
validateSections(errors, entries, section, localStack, fullUrl, id);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void validateDocumentSubReference(List<ValidationMessage> errors, List<Element> entries, Element composition, NodeStack stack, String fullUrl, String id, String title, String parent, boolean repeats, String propName) {
|
||||
List<Element> list = new ArrayList<>();
|
||||
composition.getNamedChildren(parent, list);
|
||||
int i = 1;
|
||||
for (Element elem : list) {
|
||||
validateDocumentReference(errors, entries, elem, stack.push(elem, i, null, null), fullUrl, id, repeats, propName, title + "." + parent);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
public void validateDocumentReference(List<ValidationMessage> errors, List<Element> entries, Element composition, NodeStack stack, String fullUrl, String id, boolean repeats, String propName, String title) {
|
||||
if (repeats) {
|
||||
List<Element> list = new ArrayList<>();
|
||||
composition.getNamedChildren(propName, list);
|
||||
int i = 1;
|
||||
for (Element elem : list) {
|
||||
|
||||
validateBundleReference(errors, entries, elem, title + "." + propName, stack.push(elem, i, null, null), fullUrl, "Composition", id);
|
||||
i++;
|
||||
}
|
||||
|
||||
} else {
|
||||
Element elem = composition.getNamedChild(propName);
|
||||
if (elem != null) {
|
||||
validateBundleReference(errors, entries, elem, title + "." + propName, stack.push(elem, -1, null, null), fullUrl, "Composition", id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void validateMessage(List<ValidationMessage> errors, List<Element> entries, Element messageHeader, NodeStack stack, String fullUrl, String id) {
|
||||
// first entry must be a messageheader
|
||||
if (rule(errors, IssueType.INVALID, messageHeader.line(), messageHeader.col(), stack.getLiteralPath(), messageHeader.getType().equals("MessageHeader"), I18nConstants.VALIDATION_BUNDLE_MESSAGE)) {
|
||||
List<Element> elements = messageHeader.getChildren("focus");
|
||||
for (Element elem : elements)
|
||||
validateBundleReference(errors, entries, elem, "MessageHeader Data", stack.push(elem, -1, null, null), fullUrl, "MessageHeader", id);
|
||||
}
|
||||
}
|
||||
|
||||
private void validateBundleReference(List<ValidationMessage> errors, List<Element> entries, Element ref, String name, NodeStack stack, String fullUrl, String type, String id) {
|
||||
String reference = null;
|
||||
try {
|
||||
reference = ref.getNamedChildValue("reference");
|
||||
} catch (Error e) {
|
||||
|
||||
}
|
||||
|
||||
if (ref != null && !Utilities.noString(reference) && !reference.startsWith("#")) {
|
||||
Element target = resolveInBundle(entries, reference, fullUrl, type, id);
|
||||
rule(errors, IssueType.INVALID, ref.line(), ref.col(), stack.addToLiteralPath("reference"), target != null, I18nConstants.BUNDLE_BUNDLE_ENTRY_NOTFOUND, reference, name);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* As per outline for <a href=http://hl7.org/fhir/stu3/documents.html#content>Document Content</a>:
|
||||
* <li>"The document date (mandatory). This is found in Bundle.meta.lastUpdated and identifies when the document bundle
|
||||
* was assembled from the underlying resources"</li>
|
||||
* <p></p>
|
||||
* This check was not being done for release versions < r4.
|
||||
* <p></p>
|
||||
* Related JIRA ticket is <a href=https://jira.hl7.org/browse/FHIR-26544>FHIR-26544</a>
|
||||
*
|
||||
* @param bundle {@link org.hl7.fhir.r5.elementmodel}
|
||||
* @param errors {@link List<ValidationMessage>}
|
||||
* @param stack {@link NodeStack}
|
||||
*/
|
||||
private void handleSpecialCaseForLastUpdated(Element bundle, List<ValidationMessage> errors, NodeStack stack) {
|
||||
boolean ok = bundle.hasChild(META)
|
||||
&& bundle.getNamedChild(META).hasChild(LAST_UPDATED)
|
||||
&& bundle.getNamedChild(META).getNamedChild(LAST_UPDATED).hasValue();
|
||||
rule(errors, IssueType.REQUIRED, stack.getLiteralPath(), ok, I18nConstants.DOCUMENT_DATE_REQUIRED, I18nConstants.DOCUMENT_DATE_REQUIRED_HTML);
|
||||
}
|
||||
|
||||
private void checkAllInterlinked(List<ValidationMessage> errors, List<Element> entries, NodeStack stack, Element bundle, boolean isError) {
|
||||
List<EntrySummary> entryList = new ArrayList<>();
|
||||
for (Element entry : entries) {
|
||||
Element r = entry.getNamedChild(RESOURCE);
|
||||
if (r != null) {
|
||||
entryList.add(new EntrySummary(entry, r));
|
||||
}
|
||||
}
|
||||
for (EntrySummary e : entryList) {
|
||||
Set<String> references = findReferences(e.getEntry());
|
||||
for (String ref : references) {
|
||||
Element tgt = resolveInBundle(entries, ref, e.getEntry().getChildValue(FULL_URL), e.getResource().fhirType(), e.getResource().getIdBase());
|
||||
if (tgt != null) {
|
||||
EntrySummary t = entryForTarget(entryList, tgt);
|
||||
if (t != null) {
|
||||
e.getTargets().add(t);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Set<EntrySummary> visited = new HashSet<>();
|
||||
visitLinked(visited, entryList.get(0));
|
||||
boolean foundRevLinks;
|
||||
do {
|
||||
foundRevLinks = false;
|
||||
for (EntrySummary e : entryList) {
|
||||
if (!visited.contains(e)) {
|
||||
boolean add = false;
|
||||
for (EntrySummary t : e.getTargets()) {
|
||||
if (visited.contains(t)) {
|
||||
add = true;
|
||||
}
|
||||
}
|
||||
if (add) {
|
||||
foundRevLinks = true;
|
||||
visitLinked(visited, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
} while (foundRevLinks);
|
||||
|
||||
int i = 0;
|
||||
for (EntrySummary e : entryList) {
|
||||
Element entry = e.getEntry();
|
||||
if (isError) {
|
||||
rule(errors, IssueType.INFORMATIONAL, entry.line(), entry.col(), stack.addToLiteralPath(ENTRY + '[' + (i + 1) + ']'), visited.contains(e), I18nConstants.BUNDLE_BUNDLE_ENTRY_ORPHAN, (entry.getChildValue(FULL_URL) != null ? "'" + entry.getChildValue(FULL_URL) + "'" : ""));
|
||||
} else {
|
||||
warning(errors, IssueType.INFORMATIONAL, entry.line(), entry.col(), stack.addToLiteralPath(ENTRY + '[' + (i + 1) + ']'), visited.contains(e), I18nConstants.BUNDLE_BUNDLE_ENTRY_ORPHAN, (entry.getChildValue(FULL_URL) != null ? "'" + entry.getChildValue(FULL_URL) + "'" : ""));
|
||||
}
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
private String uriRegexForVersion() {
|
||||
if (VersionUtilities.isR3Ver(context.getVersion()))
|
||||
return URI_REGEX3;
|
||||
else
|
||||
return Constants.URI_REGEX;
|
||||
}
|
||||
|
||||
private String getCanonicalURLForEntry(Element entry) {
|
||||
Element e = entry.getNamedChild(RESOURCE);
|
||||
if (e == null)
|
||||
return null;
|
||||
return e.getNamedChildValue("url");
|
||||
}
|
||||
|
||||
private String getIdForEntry(Element entry) {
|
||||
Element e = entry.getNamedChild(RESOURCE);
|
||||
if (e == null)
|
||||
return null;
|
||||
return e.getNamedChildValue(ID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check each resource entry to ensure that the entry's fullURL includes the resource's id
|
||||
* value. Adds an ERROR ValidationMessge to errors List for a given entry if it references
|
||||
* a resource and fullURL does not include the resource's id.
|
||||
*
|
||||
* @param errors List of ValidationMessage objects that new errors will be added to.
|
||||
* @param entries List of entry Element objects to be checked.
|
||||
* @param stack Current NodeStack used to create path names in error detail messages.
|
||||
*/
|
||||
private void validateResourceIds(List<ValidationMessage> errors, List<Element> entries, NodeStack stack) {
|
||||
// TODO: Need to handle _version
|
||||
int i = 1;
|
||||
for (Element entry : entries) {
|
||||
String fullUrl = entry.getNamedChildValue(FULL_URL);
|
||||
Element resource = entry.getNamedChild(RESOURCE);
|
||||
String id = resource != null ? resource.getNamedChildValue(ID) : null;
|
||||
if (id != null && fullUrl != null) {
|
||||
String urlId = null;
|
||||
if (fullUrl.startsWith("https://") || fullUrl.startsWith("http://")) {
|
||||
urlId = fullUrl.substring(fullUrl.lastIndexOf('/') + 1);
|
||||
} else if (fullUrl.startsWith("urn:uuid") || fullUrl.startsWith("urn:oid")) {
|
||||
urlId = fullUrl.substring(fullUrl.lastIndexOf(':') + 1);
|
||||
}
|
||||
rule(errors, IssueType.INVALID, entry.line(), entry.col(), stack.addToLiteralPath("entry[" + i + "]"), urlId.equals(id), I18nConstants.BUNDLE_BUNDLE_ENTRY_IDURLMISMATCH, id, fullUrl);
|
||||
}
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
private EntrySummary entryForTarget(List<EntrySummary> entryList, Element tgt) {
|
||||
for (EntrySummary e : entryList) {
|
||||
if (e.getEntry() == tgt) {
|
||||
return e;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void visitLinked(Set<EntrySummary> visited, EntrySummary t) {
|
||||
if (!visited.contains(t)) {
|
||||
visited.add(t);
|
||||
for (EntrySummary e : t.getTargets()) {
|
||||
visitLinked(visited, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void followResourceLinks(Element entry, Map<String, Element> visitedResources, Map<Element, Element> candidateEntries, List<Element> candidateResources, List<ValidationMessage> errors, NodeStack stack) {
|
||||
followResourceLinks(entry, visitedResources, candidateEntries, candidateResources, errors, stack, 0);
|
||||
}
|
||||
|
||||
private void followResourceLinks(Element entry, Map<String, Element> visitedResources, Map<Element, Element> candidateEntries, List<Element> candidateResources, List<ValidationMessage> errors, NodeStack stack, int depth) {
|
||||
Element resource = entry.getNamedChild(RESOURCE);
|
||||
if (visitedResources.containsValue(resource))
|
||||
return;
|
||||
|
||||
visitedResources.put(entry.getNamedChildValue(FULL_URL), resource);
|
||||
|
||||
String type = null;
|
||||
Set<String> references = findReferences(resource);
|
||||
for (String reference : references) {
|
||||
// We don't want errors when just retrieving the element as they will be caught (with better path info) in subsequent processing
|
||||
IndexedElement r = getFromBundle(stack.getElement(), reference, entry.getChildValue(FULL_URL), new ArrayList<ValidationMessage>(), stack.addToLiteralPath("entry[" + candidateResources.indexOf(resource) + "]"), type, "transaction".equals(stack.getElement().getChildValue(TYPE)));
|
||||
if (r != null && !visitedResources.containsValue(r.getMatch())) {
|
||||
followResourceLinks(candidateEntries.get(r.getMatch()), visitedResources, candidateEntries, candidateResources, errors, stack, depth + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private Set<String> findReferences(Element start) {
|
||||
Set<String> references = new HashSet<String>();
|
||||
findReferences(start, references);
|
||||
return references;
|
||||
}
|
||||
|
||||
private void findReferences(Element start, Set<String> references) {
|
||||
for (Element child : start.getChildren()) {
|
||||
if (child.getType().equals("Reference")) {
|
||||
String ref = child.getChildValue("reference");
|
||||
if (ref != null && !ref.startsWith("#"))
|
||||
references.add(ref);
|
||||
}
|
||||
if (child.getType().equals("url") || child.getType().equals("uri") || child.getType().equals("canonical")) {
|
||||
String ref = child.primitiveValue();
|
||||
if (ref != null && !ref.startsWith("#"))
|
||||
references.add(ref);
|
||||
}
|
||||
findReferences(child, references);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// hack for pre-UTG v2/v3
|
||||
private boolean isV3orV2Url(String url) {
|
||||
return url.startsWith("http://hl7.org/fhir/v3/") || url.startsWith("http://hl7.org/fhir/v2/");
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -6,11 +6,11 @@ title: FHIR Validator Release Notes
|
|||
|
||||
## Current (not released yet)
|
||||
|
||||
(no changes yet)
|
||||
* fix fatal NPE validating bundles when resource is missing
|
||||
* fix tests for R5 changes
|
||||
|
||||
## v4.2.30 (2020-05-12)
|
||||
|
||||
|
||||
* Allow mailto: urls as absolute URIs
|
||||
|
||||
## v4.2.29 (2020-05-11)
|
||||
|
|
Loading…
Reference in New Issue