diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/r5/validation/instancevalidator/InstanceValidator.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/r5/validation/instancevalidator/InstanceValidator.java index eb162d7b5..490c4d592 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/r5/validation/instancevalidator/InstanceValidator.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/r5/validation/instancevalidator/InstanceValidator.java @@ -32,7 +32,9 @@ import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Locale; import java.util.Map; +import java.util.ResourceBundle; import java.util.Set; import java.util.UUID; @@ -167,5421 +169,5366 @@ import ca.uhn.fhir.util.ObjectUtil; public class InstanceValidator extends BaseValidator implements IResourceValidator { - private class ValidatorHostServices implements IEvaluationContext { - - @Override - public Base resolveConstant(Object appContext, String name, boolean beforeContext) throws PathEngineException { - ValidatorHostContext c = (ValidatorHostContext) appContext; - if (externalHostServices != null) - return externalHostServices.resolveConstant(c.getAppContext(), name, beforeContext); - else - return null; - } - - @Override - public TypeDetails resolveConstantType(Object appContext, String name) throws PathEngineException { - ValidatorHostContext c = (ValidatorHostContext) appContext; - if (externalHostServices != null) - return externalHostServices.resolveConstantType(c.getAppContext(), name); - else - return null; - } - - @Override - public boolean log(String argument, List focus) { - if (externalHostServices != null) - return externalHostServices.log(argument, focus); - else - return false; - } - - @Override - public FunctionDetails resolveFunction(String functionName) { - throw new Error("Not done yet (ValidatorHostServices.resolveFunction): " + functionName); - } - - @Override - public TypeDetails checkFunction(Object appContext, String functionName, List parameters) throws PathEngineException { - throw new Error("Not done yet (ValidatorHostServices.checkFunction)"); - } - - @Override - public List executeFunction(Object appContext, String functionName, List> parameters) { - throw new Error("Not done yet (ValidatorHostServices.executeFunction)"); - } - - @Override - public Base resolveReference(Object appContext, String url, Base refContext) throws FHIRException { - ValidatorHostContext c = (ValidatorHostContext) appContext; - - if (refContext != null && refContext.hasUserData("validator.bundle.resolution")) { - return (Base) refContext.getUserData("validator.bundle.resolution"); - } - - if (c.getAppContext() instanceof Element) { - Element bnd = (Element) c.getAppContext(); - Base res = resolveInBundle(url, bnd); - if (res != null) - return res; - } - Base res = resolveInBundle(url, c.getResource()); - if (res != null) - return res; - res = resolveInBundle(url, c.getContainer()); - if (res != null) - return res; - - if (externalHostServices != null) - return externalHostServices.resolveReference(c.getAppContext(), url, refContext); - else if (fetcher != null) - try { - return fetcher.fetch(c.getAppContext(), url); - } catch (IOException e) { - throw new FHIRException(e); - } - else - throw new Error("Not done yet - resolve " + url + " locally (2)"); - - } - - 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("fullUrl"); - 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; - StructureDefinition sd = context.fetchResource(StructureDefinition.class, url); - if (sd == null) { - throw new FHIRException("Unable to resolve " + url); - } - InstanceValidator self = InstanceValidator.this; - List valerrors = new ArrayList(); - if (item instanceof Resource) { - try { - Element e = new ObjectConverter(context).convert((Resource) item); - self.validateResource(new ValidatorHostContext(ctxt.getAppContext(), e), valerrors, e, e, sd, IdStatus.OPTIONAL, new NodeStack(e)); - } catch (IOException e1) { - throw new FHIRException(e1); - } - } else if (item instanceof Element) { - Element e = (Element) item; - if (e.isResource()) { - self.validateResource(new ValidatorHostContext(ctxt.getAppContext(), e), valerrors, e, e, sd, IdStatus.OPTIONAL, new NodeStack(e)); - } else { - throw new FHIRException("Not supported yet"); - } - } else - throw new NotImplementedException("Not done yet (ValidatorHostServices.conformsToProfile), when item is not an element"); - boolean ok = true; - List record = new ArrayList<>(); - for (ValidationMessage v : valerrors) { - ok = ok && !v.getLevel().isError(); - if (v.getLevel().isError() || v.isSlicingHint()) { - record.add(v); - } - } - if (!ok && !record.isEmpty()) { - ctxt.sliceNotes(url, record); - } - return ok; - } - - @Override - public ValueSet resolveValueSet(Object appContext, String url) { - ValidatorHostContext c = (ValidatorHostContext) appContext; - if (c.getProfile() != null && url.startsWith("#")) { - for (Resource r : c.getProfile().getContained()) { - if (r.getId().equals(url.substring(1))) { - if (r instanceof ValueSet) - return (ValueSet) r; - else - throw new FHIRException("Reference " + url + " refers to a " + r.fhirType() + " not a ValueSet"); - } - } - return null; - } - return context.fetchResource(ValueSet.class, url); - } - - } - - private IWorkerContext context; - private FHIRPathEngine fpe; - - // configuration items - private CheckDisplayOption checkDisplay; - private boolean anyExtensionsAllowed; - private boolean errorForUnknownProfiles; - private boolean noInvariantChecks; - private boolean noTerminologyChecks; - private boolean hintAboutNonMustSupport; - private boolean showMessagesFromReferences; - private BestPracticeWarningLevel bpWarnings; - private String validationLanguage; - private boolean baseOnly; - - private List extensionDomains = new ArrayList(); - - private IdStatus resourceIdRule; - private boolean allowXsiLocation; - - // used during the build process to keep the overall volume of messages down - private boolean suppressLoincSnomedMessages; - - // time tracking - private long overall = 0; - private long txTime = 0; - private long sdTime = 0; - private long loadTime = 0; - private long fpeTime = 0; - - private boolean noBindingMsgSuppressed; - private boolean debug; - private Map fetchCache = new HashMap<>(); - private HashMap resourceTracker = new HashMap<>(); - private IValidatorResourceFetcher fetcher; - long time = 0; - private IEvaluationContext externalHostServices; - private boolean noExtensibleWarnings; - private String serverBase; - - private EnableWhenEvaluator myEnableWhenEvaluator = new EnableWhenEvaluator(); - private String executionId; - private XVerExtensionManager xverManager; - private IValidationProfileUsageTracker tracker; - private ValidatorHostServices validatorServices; - private boolean assumeValidRestReferences; - private boolean allowExamples; - - public InstanceValidator(IWorkerContext theContext, IEvaluationContext hostServices) { - super(); - this.context = theContext; - this.externalHostServices = hostServices; - fpe = new FHIRPathEngine(context); - validatorServices = new ValidatorHostServices(); - fpe.setHostServices(validatorServices); - if (theContext.getVersion().startsWith("3.0") || theContext.getVersion().startsWith("1.0")) - fpe.setLegacyMode(true); - source = Source.InstanceValidator; - } + private class ValidatorHostServices implements IEvaluationContext { @Override - public boolean isNoExtensibleWarnings() { - return noExtensibleWarnings; - } - - @Override - public IResourceValidator setNoExtensibleWarnings(boolean noExtensibleWarnings) { - this.noExtensibleWarnings = noExtensibleWarnings; - return this; - } - - @Override - public boolean isShowMessagesFromReferences() { - return showMessagesFromReferences; - } - - @Override - public void setShowMessagesFromReferences(boolean showMessagesFromReferences) { - this.showMessagesFromReferences = showMessagesFromReferences; - } - - @Override - public boolean isNoInvariantChecks() { - return noInvariantChecks; - } - - @Override - public IResourceValidator setNoInvariantChecks(boolean value) { - this.noInvariantChecks = value; - return this; - } - - public IValidatorResourceFetcher getFetcher() { - return this.fetcher; - } - - public IResourceValidator setFetcher(IValidatorResourceFetcher value) { - this.fetcher = value; - return this; - } - - public IValidationProfileUsageTracker getTracker() { - return this.tracker; - } - - public IResourceValidator setTracker(IValidationProfileUsageTracker value) { - this.tracker = value; - return this; - } - - - public boolean isHintAboutNonMustSupport() { - return hintAboutNonMustSupport; - } - - public void setHintAboutNonMustSupport(boolean hintAboutNonMustSupport) { - this.hintAboutNonMustSupport = hintAboutNonMustSupport; - } - - public boolean isAssumeValidRestReferences() { - return this.assumeValidRestReferences; - } - - public void setAssumeValidRestReferences(boolean value) { - this.assumeValidRestReferences = value; - } - - public boolean isAllowExamples() { - return this.allowExamples; - } - - public void setAllowExamples(boolean value) { - this.allowExamples = value; - } - - - private boolean allowUnknownExtension(String url) { - if ((allowExamples && (url.contains("example.org") || url.contains("acme.com"))) || url.contains("nema.org") || url.startsWith("http://hl7.org/fhir/tools/StructureDefinition/") || url.equals("http://hl7.org/fhir/StructureDefinition/structuredefinition-expression")) - // Added structuredefinition-expression explicitly because it wasn't defined in the version of the spec it needs to be used with - return true; - for (String s : extensionDomains) - if (url.startsWith(s)) - return true; - return anyExtensionsAllowed; - } - - private boolean isKnownExtension(String url) { - // Added structuredefinition-expression and following extensions explicitly because they weren't defined in the version of the spec they need to be used with - if ((allowExamples && (url.contains("example.org") || url.contains("acme.com"))) || url.contains("nema.org") || url.startsWith("http://hl7.org/fhir/tools/StructureDefinition/") || url.equals("http://hl7.org/fhir/StructureDefinition/structuredefinition-expression") || url.equals(VersionConvertorConstants.IG_DEPENDSON_PACKAGE_EXTENSION)) - return true; - for (String s : extensionDomains) - if (url.startsWith(s)) - return true; - return false; - } - - private void bpCheck(List errors, IssueType invalid, int line, int col, String literalPath, boolean test, String message) { - if (bpWarnings != null) { - switch (bpWarnings) { - case Error: - rule(errors, invalid, line, col, literalPath, test, message); - break; - case Warning: - warning(errors, invalid, line, col, literalPath, test, message); - break; - case Hint: - hint(errors, invalid, line, col, literalPath, test, message); - break; - default: // do nothing - break; - } - } - } - - @Override - public org.hl7.fhir.r5.elementmodel.Element validate(Object appContext, List errors, InputStream stream, FhirFormat format) throws FHIRException { - return validate(appContext, errors, stream, format, new ArrayList<>()); - } - - @Override - public org.hl7.fhir.r5.elementmodel.Element validate(Object appContext, List errors, InputStream stream, FhirFormat format, String profile) throws FHIRException { - ArrayList profiles = new ArrayList<>(); - if (profile != null) { - profiles.add(getSpecifiedProfile(profile)); - } - return validate(appContext, errors, stream, format, profiles); - } - - private StructureDefinition getSpecifiedProfile(String profile) { - StructureDefinition sd = context.fetchResource(StructureDefinition.class, profile); - if (sd == null) { - throw new FHIRException("Unable to locate the profile '" + profile + "' in order to validate against it"); - } - return sd; - } - - @Override - public org.hl7.fhir.r5.elementmodel.Element validate(Object appContext, List errors, InputStream stream, FhirFormat format, List profiles) throws FHIRException { - ParserBase parser = Manager.makeParser(context, format); - if (parser instanceof XmlParser) - ((XmlParser) parser).setAllowXsiLocation(allowXsiLocation); - parser.setupValidation(ValidationPolicy.EVERYTHING, errors); - long t = System.nanoTime(); - Element e; - try { - e = parser.parse(stream); - } catch (IOException e1) { - throw new FHIRException(e1); - } - loadTime = System.nanoTime() - t; - if (e != null) - validate(appContext, errors, e, profiles); - return e; - } - - @Override - public org.hl7.fhir.r5.elementmodel.Element validate(Object appContext, List errors, Resource resource) throws FHIRException { - return validate(appContext, errors, resource, new ArrayList<>()); - } - - @Override - public org.hl7.fhir.r5.elementmodel.Element validate(Object appContext, List errors, Resource resource, String profile) throws FHIRException { - ArrayList profiles = new ArrayList<>(); - if (profile != null) { - profiles.add(getSpecifiedProfile(profile)); - } - return validate(appContext, errors, resource, profiles); - } - - @Override - public org.hl7.fhir.r5.elementmodel.Element validate(Object appContext, List errors, Resource resource, List profiles) throws FHIRException { - long t = System.nanoTime(); - Element e; - try { - e = new ObjectConverter(context).convert(resource); - } catch (IOException e1) { - throw new FHIRException(e1); - } - loadTime = System.nanoTime() - t; - validate(appContext, errors, e, profiles); - return e; - } - - @Override - public org.hl7.fhir.r5.elementmodel.Element validate(Object appContext, List errors, org.w3c.dom.Element element) throws FHIRException { - return validate(appContext, errors, element, new ArrayList<>()); - } - - @Override - public org.hl7.fhir.r5.elementmodel.Element validate(Object appContext, List errors, org.w3c.dom.Element element, String profile) throws FHIRException { - ArrayList profiles = new ArrayList<>(); - if (profile != null) { - profiles.add(getSpecifiedProfile(profile)); - } - return validate(appContext, errors, element, profiles); - } - - @Override - public org.hl7.fhir.r5.elementmodel.Element validate(Object appContext, List errors, org.w3c.dom.Element element, List profiles) throws FHIRException { - XmlParser parser = new XmlParser(context); - parser.setupValidation(ValidationPolicy.EVERYTHING, errors); - long t = System.nanoTime(); - Element e; - try { - e = parser.parse(element); - } catch (IOException e1) { - throw new FHIRException(e1); - } - loadTime = System.nanoTime() - t; - if (e != null) - validate(appContext, errors, e, profiles); - return e; - } - - @Override - public org.hl7.fhir.r5.elementmodel.Element validate(Object appContext, List errors, Document document) throws FHIRException { - return validate(appContext, errors, document, new ArrayList<>()); - } - - @Override - public org.hl7.fhir.r5.elementmodel.Element validate(Object appContext, List errors, Document document, String profile) throws FHIRException { - ArrayList profiles = new ArrayList<>(); - if (profile != null) { - profiles.add(getSpecifiedProfile(profile)); - } - return validate(appContext, errors, document, profiles); - } - - @Override - public org.hl7.fhir.r5.elementmodel.Element validate(Object appContext, List errors, Document document, List profiles) throws FHIRException { - XmlParser parser = new XmlParser(context); - parser.setupValidation(ValidationPolicy.EVERYTHING, errors); - long t = System.nanoTime(); - Element e; - try { - e = parser.parse(document); - } catch (IOException e1) { - throw new FHIRException(e1); - } - loadTime = System.nanoTime() - t; - if (e != null) - validate(appContext, errors, e, profiles); - return e; - } - - @Override - public org.hl7.fhir.r5.elementmodel.Element validate(Object appContext, List errors, JsonObject object) throws FHIRException { - return validate(appContext, errors, object, new ArrayList<>()); - } - - @Override - public org.hl7.fhir.r5.elementmodel.Element validate(Object appContext, List errors, JsonObject object, String profile) throws FHIRException { - ArrayList profiles = new ArrayList<>(); - if (profile != null) { - profiles.add(getSpecifiedProfile(profile)); - } - return validate(appContext, errors, object, profiles); - } - - @Override - public org.hl7.fhir.r5.elementmodel.Element validate(Object appContext, List errors, JsonObject object, List profiles) throws FHIRException { - JsonParser parser = new JsonParser(context); - parser.setupValidation(ValidationPolicy.EVERYTHING, errors); - long t = System.nanoTime(); - Element e = parser.parse(object); - loadTime = System.nanoTime() - t; - if (e != null) - validate(appContext, errors, e, profiles); - return e; - } - - @Override - public void validate(Object appContext, List errors, Element element) throws FHIRException { - validate(appContext, errors, element, new ArrayList<>()); - } - - @Override - public void validate(Object appContext, List errors, Element element, String profile) throws FHIRException { - ArrayList profiles = new ArrayList<>(); - if (profile != null) { - profiles.add(getSpecifiedProfile(profile)); - } - validate(appContext, errors, element, profiles); - } - - @Override - public void validate(Object appContext, List errors, Element element, List profiles) throws FHIRException { - // this is the main entry point; all the other public entry points end up here coming here... - // so the first thing to do is to clear the internal state - fetchCache.clear(); - fetchCache.put(element.fhirType() + "/" + element.getIdBase(), element); - resourceTracker.clear(); - executionId = UUID.randomUUID().toString(); - baseOnly = profiles.isEmpty(); - - long t = System.nanoTime(); - if (profiles == null || profiles.isEmpty()) { - validateResource(new ValidatorHostContext(appContext, element), errors, element, element, null, resourceIdRule, new NodeStack(element)); - } else { - for (StructureDefinition defn : profiles) { - validateResource(new ValidatorHostContext(appContext, element), errors, element, element, defn, resourceIdRule, new NodeStack(element)); - } - } - if (hintAboutNonMustSupport) { - checkElementUsage(errors, element, new NodeStack(element)); - } - overall = System.nanoTime() - t; - } - - private void checkElementUsage(List errors, Element element, NodeStack stack) { - String elementUsage = element.getUserString("elementSupported"); - hint(errors, IssueType.INFORMATIONAL, element.line(), element.col(), stack.getLiteralPath(), elementUsage == null || elementUsage.equals("Y"), - "The element " + element.getName() + " is not marked as 'mustSupport' in the profile " + element.getProperty().getStructure().getUrl() + ". Consider not using the element, or marking the element as must-Support in the profile"); - - if (element.hasChildren()) { - String prevName = ""; - int elementCount = 0; - for (Element ce : element.getChildren()) { - if (ce.getName().equals(prevName)) - elementCount++; - else { - elementCount = 1; - prevName = ce.getName(); - } - checkElementUsage(errors, ce, stack.push(ce, elementCount, null, null)); - } - } - } - - private boolean check(String v1, String v2) { - return v1 == null ? Utilities.noString(v1) : v1.equals(v2); - } - - private void checkAddress(List errors, String path, Element focus, Address fixed, String fixedSource, boolean pattern) { - checkFixedValue(errors, path + ".use", focus.getNamedChild("use"), fixed.getUseElement(), fixedSource, "use", focus, pattern); - checkFixedValue(errors, path + ".text", focus.getNamedChild("text"), fixed.getTextElement(), fixedSource, "text", focus, pattern); - checkFixedValue(errors, path + ".city", focus.getNamedChild("city"), fixed.getCityElement(), fixedSource, "city", focus, pattern); - checkFixedValue(errors, path + ".state", focus.getNamedChild("state"), fixed.getStateElement(), fixedSource, "state", focus, pattern); - checkFixedValue(errors, path + ".country", focus.getNamedChild("country"), fixed.getCountryElement(), fixedSource, "country", focus, pattern); - checkFixedValue(errors, path + ".zip", focus.getNamedChild("zip"), fixed.getPostalCodeElement(), fixedSource, "postalCode", focus, pattern); - - List lines = new ArrayList(); - focus.getNamedChildren("line", lines); - if (rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, lines.size() == fixed.getLine().size(), - "Expected " + Integer.toString(fixed.getLine().size()) + " but found " + Integer.toString(lines.size()) + " line elements")) { - for (int i = 0; i < lines.size(); i++) - checkFixedValue(errors, path + ".coding", lines.get(i), fixed.getLine().get(i), fixedSource, "coding", focus, pattern); - } - } - - private void checkAttachment(List errors, String path, Element focus, Attachment fixed, String fixedSource, boolean pattern) { - checkFixedValue(errors, path + ".contentType", focus.getNamedChild("contentType"), fixed.getContentTypeElement(), fixedSource, "contentType", focus, pattern); - checkFixedValue(errors, path + ".language", focus.getNamedChild("language"), fixed.getLanguageElement(), fixedSource, "language", focus, pattern); - checkFixedValue(errors, path + ".data", focus.getNamedChild("data"), fixed.getDataElement(), fixedSource, "data", focus, pattern); - checkFixedValue(errors, path + ".url", focus.getNamedChild("url"), fixed.getUrlElement(), fixedSource, "url", focus, pattern); - checkFixedValue(errors, path + ".size", focus.getNamedChild("size"), fixed.getSizeElement(), fixedSource, "size", focus, pattern); - checkFixedValue(errors, path + ".hash", focus.getNamedChild("hash"), fixed.getHashElement(), fixedSource, "hash", focus, pattern); - checkFixedValue(errors, path + ".title", focus.getNamedChild("title"), fixed.getTitleElement(), fixedSource, "title", focus, pattern); - } - - // public API - private boolean checkCode(List errors, Element element, String path, String code, String system, String display, boolean checkDisplay, NodeStack stack) throws TerminologyServiceException { - long t = System.nanoTime(); - boolean ss = context.supportsSystem(system); - txTime = txTime + (System.nanoTime() - t); - if (ss) { - t = System.nanoTime(); - ValidationResult s = context.validateCode(new ValidationOptions(stack.workingLang), system, code, checkDisplay ? display : null); - txTime = txTime + (System.nanoTime() - t); - if (s == null) - return true; - if (s.isOk()) { - if (s.getMessage() != null) - txWarning(errors, s.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, s == null, s.getMessage()); - return true; - } - if (s.getErrorClass() != null && s.getErrorClass().isInfrastructure()) - txWarning(errors, s.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, s == null, s.getMessage()); - else if (s.getSeverity() == IssueSeverity.INFORMATION) - txHint(errors, s.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, s == null, s.getMessage()); - else if (s.getSeverity() == IssueSeverity.WARNING) - txWarning(errors, s.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, s == null, s.getMessage()); - else - return txRule(errors, s.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, s == null, s.getMessage() + " for '" + system + "#" + code + "'"); - return true; - } else if (system.startsWith("http://hl7.org/fhir")) { - if (Utilities.existsInList(system, "http://hl7.org/fhir/sid/icd-10", "http://hl7.org/fhir/sid/cvx", "http://hl7.org/fhir/sid/icd-10", "http://hl7.org/fhir/sid/icd-10-cm", "http://hl7.org/fhir/sid/icd-9", "http://hl7.org/fhir/sid/ndc", "http://hl7.org/fhir/sid/srt")) - return true; // else don't check these (for now) - else if (system.startsWith("http://hl7.org/fhir/test")) - return true; // we don't validate these - else { - CodeSystem cs = getCodeSystem(system); - if (rule(errors, IssueType.CODEINVALID, element.line(), element.col(), path, cs != null, "Unknown Code System '" + system + "'")) { - ConceptDefinitionComponent def = getCodeDefinition(cs, code); - if (warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, def != null, "Unknown Code (" + system + "#" + code + ")")) - return warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, display == null || display.equals(def.getDisplay()), "Display should be '" + def.getDisplay() + "'"); - } - return false; - } - } else if (context.isNoTerminologyServer() && Utilities.existsInList(system, "http://loinc.org", "http://unitsofmeasure.org", "http://snomed.info/sct", "http://www.nlm.nih.gov/research/umls/rxnorm")) { - return true; // no checks in this case - } else if (startsWithButIsNot(system, "http://snomed.info/sct", "http://loinc.org", "http://unitsofmeasure.org", "http://www.nlm.nih.gov/research/umls/rxnorm")) { - rule(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "Invalid System URI: " + system); - return false; - } else { - try { - if (context.fetchResourceWithException(ValueSet.class, system) != null) { - rule(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "Invalid System URI: " + system + " - cannot use a value set URI as a system"); - // Lloyd: This error used to prohibit checking for downstream issues, but there are some cases where that checking needs to occur. Please talk to me before changing the code back. - } - hint(errors, IssueType.UNKNOWN, element.line(), element.col(), path, false, "Code System URI '" + system + "' is unknown so the code cannot be validated"); - return true; - } catch (Exception e) { - return true; - } - } - } - - private boolean startsWithButIsNot(String system, String... uri) { - for (String s : uri) - if (!system.equals(s) && system.startsWith(s)) - return true; - return false; - } - - - private boolean hasErrors(List errors) { - if (errors != null) { - for (ValidationMessage vm : errors) { - if (vm.getLevel() == IssueSeverity.FATAL || vm.getLevel() == IssueSeverity.ERROR) { - return true; - } - } - } - return false; - } - - private void checkCodeableConcept(List errors, String path, Element focus, CodeableConcept fixed, String fixedSource, boolean pattern) { - checkFixedValue(errors, path + ".text", focus.getNamedChild("text"), fixed.getTextElement(), fixedSource, "text", focus, pattern); - List codings = new ArrayList(); - focus.getNamedChildren("coding", codings); - if (pattern) { - if (rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, codings.size() >= fixed.getCoding().size(), - "Expected " + Integer.toString(fixed.getCoding().size()) + " but found " + Integer.toString(codings.size()) - + " coding elements")) { - for (int i = 0; i < fixed.getCoding().size(); i++) { - Coding fixedCoding = fixed.getCoding().get(i); - boolean found = false; - List allErrorsFixed = new ArrayList<>(); - List errorsFixed; - for (int j = 0; j < codings.size() && !found; ++j) { - errorsFixed = new ArrayList<>(); - checkFixedValue(errorsFixed, path + ".coding", codings.get(j), fixedCoding, fixedSource, "coding", focus, pattern); - if (!hasErrors(errorsFixed)) { - found = true; - } else { - errorsFixed - .stream() - .filter(t -> t.getLevel().ordinal() >= IssueSeverity.ERROR.ordinal()) - .forEach(t -> allErrorsFixed.add(t)); - } - } - if (!found) { - // The argonaut DSTU2 labs profile requires userSelected=false on the category.coding and this - // needs to produce an understandable error message - String message = "Expected CodeableConcept " + (pattern ? "pattern" : "fixed value") + " not found for" + - " system: " + fixedCoding.getSystemElement().asStringValue() + - " code: " + fixedCoding.getCodeElement().asStringValue() + - " display: " + fixedCoding.getDisplayElement().asStringValue(); - if (fixedCoding.hasUserSelected()) { - message += " userSelected: " + fixedCoding.getUserSelected(); - } - message += " - Issues: " + allErrorsFixed; - rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, false, message); - } - } - } - } else { - if (rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, codings.size() == fixed.getCoding().size(), - "Expected " + Integer.toString(fixed.getCoding().size()) + " but found " + Integer.toString(codings.size()) - + " coding elements")) { - for (int i = 0; i < codings.size(); i++) - checkFixedValue(errors, path + ".coding", codings.get(i), fixed.getCoding().get(i), fixedSource, "coding", focus); - } - } - } - - private boolean checkCodeableConcept(List errors, String path, Element element, StructureDefinition profile, ElementDefinition theElementCntext, NodeStack stack) { - boolean res = true; - if (!noTerminologyChecks && theElementCntext != null && theElementCntext.hasBinding()) { - ElementDefinitionBindingComponent binding = theElementCntext.getBinding(); - if (warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, binding != null, "Binding for " + path + " missing (cc)")) { - if (binding.hasValueSet()) { - ValueSet valueset = resolveBindingReference(profile, binding.getValueSet(), profile.getUrl()); - if (warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, valueset != null, "ValueSet " + describeReference(binding.getValueSet()) + " not found by validator")) { - try { - CodeableConcept cc = ObjectConverter.readAsCodeableConcept(element); - if (!cc.hasCoding()) { - if (binding.getStrength() == BindingStrength.REQUIRED) - rule(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "No code provided, and a code is required from the value set " + describeReference(binding.getValueSet()) + " (" + valueset.getUrl()); - else if (binding.getStrength() == BindingStrength.EXTENSIBLE) { - if (binding.hasExtension("http://hl7.org/fhir/StructureDefinition/elementdefinition-maxValueSet")) - rule(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "No code provided, and a code must be provided from the value set " + describeReference(ToolingExtensions.readStringExtension(binding, "http://hl7.org/fhir/StructureDefinition/elementdefinition-maxValueSet")) + " (max value set " + valueset.getUrl() + ")"); - else - warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "No code provided, and a code should be provided from the value set " + describeReference(binding.getValueSet()) + " (" + valueset.getUrl() + ")"); - } - } else { - long t = System.nanoTime(); - - // Check whether the codes are appropriate for the type of binding we have - boolean bindingsOk = true; - if (binding.getStrength() != BindingStrength.EXAMPLE) { - boolean atLeastOneSystemIsSupported = false; - for (Coding nextCoding : cc.getCoding()) { - String nextSystem = nextCoding.getSystem(); - if (isNotBlank(nextSystem) && context.supportsSystem(nextSystem)) { - atLeastOneSystemIsSupported = true; - break; - } - } - - if (!atLeastOneSystemIsSupported && binding.getStrength() == BindingStrength.EXAMPLE) { - // ignore this since we can't validate but it doesn't matter.. - } else { - ValidationResult vr = context.validateCode(new ValidationOptions(stack.workingLang).checkValueSetOnly(), cc, valueset); // we're going to validate the codings directly, so only check the valueset - if (!vr.isOk()) { - bindingsOk = false; - if (vr.getErrorClass() != null && vr.getErrorClass().isInfrastructure()) { - if (binding.getStrength() == BindingStrength.REQUIRED) - txWarning(errors, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false, "Could not confirm that the codes provided are in the value set " + describeReference(binding.getValueSet()) + " and a code from this value set is required (class = " + vr.getErrorClass().toString() + ")"); - else if (binding.getStrength() == BindingStrength.EXTENSIBLE) { - if (binding.hasExtension("http://hl7.org/fhir/StructureDefinition/elementdefinition-maxValueSet")) - checkMaxValueSet(errors, path, element, profile, ToolingExtensions.readStringExtension(binding, "http://hl7.org/fhir/StructureDefinition/elementdefinition-maxValueSet"), cc, stack); - else if (!noExtensibleWarnings) - txWarning(errors, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false, "Could not confirm that the codes provided are in the value set " + describeReference(binding.getValueSet()) + " and a code should come from this value set unless it has no suitable code (class = " + vr.getErrorClass().toString() + ")"); - } else if (binding.getStrength() == BindingStrength.PREFERRED) { - if (baseOnly) { - txHint(errors, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false, "Could not confirm that the codes provided are in the value set " + describeReference(binding.getValueSet()) + " and a code is recommended to come from this value set (class = " + vr.getErrorClass().toString() + ")"); - } - } - } else { - if (binding.getStrength() == BindingStrength.REQUIRED) - txRule(errors, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false, "None of the codes provided are in the value set " + describeReference(binding.getValueSet()) + " (" + valueset.getUrl() + ", and a code from this value set is required) (codes = " + ccSummary(cc) + ")"); - else if (binding.getStrength() == BindingStrength.EXTENSIBLE) { - if (binding.hasExtension("http://hl7.org/fhir/StructureDefinition/elementdefinition-maxValueSet")) - checkMaxValueSet(errors, path, element, profile, ToolingExtensions.readStringExtension(binding, "http://hl7.org/fhir/StructureDefinition/elementdefinition-maxValueSet"), cc, stack); - if (!noExtensibleWarnings) - txWarning(errors, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false, "None of the codes provided are in the value set " + describeReference(binding.getValueSet()) + " (" + valueset.getUrl() + ", and a code should come from this value set unless it has no suitable code) (codes = " + ccSummary(cc) + ")"); - } else if (binding.getStrength() == BindingStrength.PREFERRED) { - if (baseOnly) { - txHint(errors, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false, "None of the codes provided are in the value set " + describeReference(binding.getValueSet()) + " (" + valueset.getUrl() + ", and a code is recommended to come from this value set) (codes = " + ccSummary(cc) + ")"); - } - } - } - } else if (vr.getMessage() != null) { - res = false; - txWarning(errors, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false, vr.getMessage()); - } else { - res = false; - } - } - // Then, for any codes that are in code systems we are able - // to validate, we'll validate that the codes actually exist - if (bindingsOk) { - for (Coding nextCoding : cc.getCoding()) { - if (isNotBlank(nextCoding.getCode()) && isNotBlank(nextCoding.getSystem()) && context.supportsSystem(nextCoding.getSystem())) { - ValidationResult vr = context.validateCode(new ValidationOptions(stack.workingLang).noCheckValueSetMembership(), nextCoding, valueset); - if (vr.getSeverity() != null) { - if (vr.getSeverity() == IssueSeverity.INFORMATION) { - txHint(errors, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false, vr.getMessage()); - } else if (vr.getSeverity() == IssueSeverity.WARNING) { - txWarning(errors, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false, vr.getMessage()); - } else { - txRule(errors, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false, vr.getMessage()); - } - } - } - } - } - txTime = txTime + (System.nanoTime() - t); - } - } - } catch (Exception e) { - warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "Error " + e.getMessage() + " validating CodeableConcept"); - } - } - } else if (binding.hasValueSet()) { - hint(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "Binding by URI reference cannot be checked"); - } else if (!noBindingMsgSuppressed) { - hint(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "Binding for path " + path + " has no source, so can't be checked"); - } - } - } - return res; - } - - private boolean checkTerminologyCodeableConcept(List errors, String path, Element element, StructureDefinition profile, ElementDefinition theElementCntext, NodeStack stack, StructureDefinition logical) { - boolean res = true; - if (!noTerminologyChecks && theElementCntext != null && theElementCntext.hasBinding()) { - ElementDefinitionBindingComponent binding = theElementCntext.getBinding(); - if (warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, binding != null, "Binding for " + path + " missing (cc)")) { - if (binding.hasValueSet()) { - ValueSet valueset = resolveBindingReference(profile, binding.getValueSet(), profile.getUrl()); - if (warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, valueset != null, "ValueSet " + describeReference(binding.getValueSet()) + " not found by validator")) { - try { - CodeableConcept cc = convertToCodeableConcept(element, logical); - if (!cc.hasCoding()) { - if (binding.getStrength() == BindingStrength.REQUIRED) - rule(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "No code provided, and a code is required from the value set " + describeReference(binding.getValueSet()) + " (" + valueset.getUrl() + ")"); - else if (binding.getStrength() == BindingStrength.EXTENSIBLE) { - if (binding.hasExtension("http://hl7.org/fhir/StructureDefinition/elementdefinition-maxValueSet")) - rule(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "No code provided, and a code must be provided from the value set " + describeReference(ToolingExtensions.readStringExtension(binding, "http://hl7.org/fhir/StructureDefinition/elementdefinition-maxValueSet")) + " (max value set " + valueset.getUrl() + ")"); - else - warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "No code provided, and a code should be provided from the value set " + describeReference(binding.getValueSet()) + " (" + valueset.getUrl() + ")"); - } - } else { - long t = System.nanoTime(); - - // Check whether the codes are appropriate for the type of binding we have - boolean bindingsOk = true; - if (binding.getStrength() != BindingStrength.EXAMPLE) { - boolean atLeastOneSystemIsSupported = false; - for (Coding nextCoding : cc.getCoding()) { - String nextSystem = nextCoding.getSystem(); - if (isNotBlank(nextSystem) && context.supportsSystem(nextSystem)) { - atLeastOneSystemIsSupported = true; - break; - } - } - - if (!atLeastOneSystemIsSupported && binding.getStrength() == BindingStrength.EXAMPLE) { - // ignore this since we can't validate but it doesn't matter.. - } else { - ValidationResult vr = context.validateCode(new ValidationOptions(stack.workingLang), cc, valueset); // we're going to validate the codings directly - if (!vr.isOk()) { - bindingsOk = false; - if (vr.getErrorClass() != null && vr.getErrorClass().isInfrastructure()) { - if (binding.getStrength() == BindingStrength.REQUIRED) - txWarning(errors, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false, "Could not confirm that the codes provided are in the value set " + describeReference(binding.getValueSet()) + " and a code from this value set is required (class = " + vr.getErrorClass().toString() + ")"); - else if (binding.getStrength() == BindingStrength.EXTENSIBLE) { - if (binding.hasExtension("http://hl7.org/fhir/StructureDefinition/elementdefinition-maxValueSet")) - checkMaxValueSet(errors, path, element, profile, ToolingExtensions.readStringExtension(binding, "http://hl7.org/fhir/StructureDefinition/elementdefinition-maxValueSet"), cc, stack); - else if (!noExtensibleWarnings) - txWarning(errors, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false, "Could not confirm that the codes provided are in the value set " + describeReference(binding.getValueSet()) + " and a code should come from this value set unless it has no suitable code (class = " + vr.getErrorClass().toString() + ")"); - } else if (binding.getStrength() == BindingStrength.PREFERRED) { - if (baseOnly) { - txHint(errors, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false, "Could not confirm that the codes provided are in the value set " + describeReference(binding.getValueSet()) + " and a code is recommended to come from this value set (class = " + vr.getErrorClass().toString() + ")"); - } - } - } else { - if (binding.getStrength() == BindingStrength.REQUIRED) - txRule(errors, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false, "None of the codes provided are in the value set " + describeReference(binding.getValueSet()) + " (" + valueset.getUrl() + ", and a code from this value set is required) (codes = " + ccSummary(cc) + ")"); - else if (binding.getStrength() == BindingStrength.EXTENSIBLE) { - if (binding.hasExtension("http://hl7.org/fhir/StructureDefinition/elementdefinition-maxValueSet")) - checkMaxValueSet(errors, path, element, profile, ToolingExtensions.readStringExtension(binding, "http://hl7.org/fhir/StructureDefinition/elementdefinition-maxValueSet"), cc, stack); - if (!noExtensibleWarnings) - txWarning(errors, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false, "None of the codes provided are in the value set " + describeReference(binding.getValueSet()) + " (" + valueset.getUrl() + ", and a code should come from this value set unless it has no suitable code) (codes = " + ccSummary(cc) + ")"); - } else if (binding.getStrength() == BindingStrength.PREFERRED) { - if (baseOnly) { - txHint(errors, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false, "None of the codes provided are in the value set " + describeReference(binding.getValueSet()) + " (" + valueset.getUrl() + ", and a code is recommended to come from this value set) (codes = " + ccSummary(cc) + ")"); - } - } - } - } else if (vr.getMessage() != null) { - res = false; - txWarning(errors, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false, vr.getMessage()); - } else { - res = false; - } - } - // Then, for any codes that are in code systems we are able - // to validate, we'll validate that the codes actually exist - if (bindingsOk) { - for (Coding nextCoding : cc.getCoding()) { - String nextCode = nextCoding.getCode(); - String nextSystem = nextCoding.getSystem(); - if (isNotBlank(nextCode) && isNotBlank(nextSystem) && context.supportsSystem(nextSystem)) { - ValidationResult vr = context.validateCode(new ValidationOptions(stack.workingLang), nextSystem, nextCode, null); - if (!vr.isOk()) { - txWarning(errors, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false, "Code {0} is not a valid code in code system {1}", nextCode, nextSystem); - } - } - } - } - txTime = txTime + (System.nanoTime() - t); - } - } - } catch (Exception e) { - warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "Error " + e.getMessage() + " validating CodeableConcept"); - } - // special case: if the logical model has both CodeableConcept and Coding mappings, we'll also check the first coding. - if (getMapping("http://hl7.org/fhir/terminology-pattern", logical, logical.getSnapshot().getElementFirstRep()).contains("Coding")) { - checkTerminologyCoding(errors, path, element, profile, theElementCntext, true, true, stack, logical); - } - } - } else if (binding.hasValueSet()) { - hint(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "Binding by URI reference cannot be checked"); - } else if (!noBindingMsgSuppressed) { - hint(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "Binding for path " + path + " has no source, so can't be checked"); - } - } - } - return res; - } - - private void checkTerminologyCoding(List errors, String path, Element element, StructureDefinition profile, ElementDefinition theElementCntext, boolean inCodeableConcept, boolean checkDisplay, NodeStack stack, StructureDefinition logical) { - Coding c = convertToCoding(element, logical); - String code = c.getCode(); - String system = c.getSystem(); - String display = c.getDisplay(); - rule(errors, IssueType.CODEINVALID, element.line(), element.col(), path, isAbsolute(system), "Coding.system must be an absolute reference, not a local reference"); - - if (system != null && code != null && !noTerminologyChecks) { - rule(errors, IssueType.CODEINVALID, element.line(), element.col(), path, !isValueSet(system), "The Coding references a value set, not a code system ('" + system + "')"); - try { - if (checkCode(errors, element, path, code, system, display, checkDisplay, stack)) - if (theElementCntext != null && theElementCntext.hasBinding()) { - ElementDefinitionBindingComponent binding = theElementCntext.getBinding(); - if (warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, binding != null, "Binding for " + path + " missing")) { - if (binding.hasValueSet()) { - ValueSet valueset = resolveBindingReference(profile, binding.getValueSet(), profile.getUrl()); - if (warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, valueset != null, "ValueSet " + describeReference(binding.getValueSet()) + " not found by validator")) { - try { - long t = System.nanoTime(); - ValidationResult vr = null; - if (binding.getStrength() != BindingStrength.EXAMPLE) { - vr = context.validateCode(new ValidationOptions(stack.workingLang), c, valueset); - } - txTime = txTime + (System.nanoTime() - t); - if (vr != null && !vr.isOk()) { - if (vr.IsNoService()) - txHint(errors, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false, "The value provided could not be validated in the absence of a terminology server"); - else if (vr.getErrorClass() != null && !vr.getErrorClass().isInfrastructure()) { - if (binding.getStrength() == BindingStrength.REQUIRED) - txWarning(errors, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false, "Could not confirm that the codes provided are in the value set " + describeReference(binding.getValueSet(), valueset) + ", and a code from this value set is required"); - else if (binding.getStrength() == BindingStrength.EXTENSIBLE) { - if (binding.hasExtension("http://hl7.org/fhir/StructureDefinition/elementdefinition-maxValueSet")) - checkMaxValueSet(errors, path, element, profile, ToolingExtensions.readStringExtension(binding, "http://hl7.org/fhir/StructureDefinition/elementdefinition-maxValueSet"), c, stack); - else if (!noExtensibleWarnings) - txWarning(errors, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false, "Could not confirm that the codes provided are in the value set " + describeReference(binding.getValueSet(), valueset) + ", and a code should come from this value set unless it has no suitable code"); - } else if (binding.getStrength() == BindingStrength.PREFERRED) { - if (baseOnly) { - txHint(errors, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false, "Could not confirm that the codes provided are in the value set " + describeReference(binding.getValueSet(), valueset) + ", and a code is recommended to come from this value set"); - } - } - } else if (binding.getStrength() == BindingStrength.REQUIRED) - txRule(errors, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false, "The Coding provided is not in the value set " + describeReference(binding.getValueSet(), valueset) + ", and a code is required from this value set" + (vr.getMessage() != null ? " (error message = " + vr.getMessage() + ")" : "")); - else if (binding.getStrength() == BindingStrength.EXTENSIBLE) { - if (binding.hasExtension("http://hl7.org/fhir/StructureDefinition/elementdefinition-maxValueSet")) - checkMaxValueSet(errors, path, element, profile, ToolingExtensions.readStringExtension(binding, "http://hl7.org/fhir/StructureDefinition/elementdefinition-maxValueSet"), c, stack); - else - txWarning(errors, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false, "The Coding provided is not in the value set " + describeReference(binding.getValueSet(), valueset) + ", and a code should come from this value set unless it has no suitable code" + (vr.getMessage() != null ? " (error message = " + vr.getMessage() + ")" : "")); - } else if (binding.getStrength() == BindingStrength.PREFERRED) { - if (baseOnly) { - txHint(errors, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false, "The Coding provided is not in the value set " + describeReference(binding.getValueSet(), valueset) + ", and a code is recommended to come from this value set" + (vr.getMessage() != null ? " (error message = " + vr.getMessage() + ")" : "")); - } - } - } - } catch (Exception e) { - warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "Error " + e.getMessage() + " validating Coding"); - } - } - } else if (binding.hasValueSet()) { - hint(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "Binding by URI reference cannot be checked"); - } else if (!inCodeableConcept && !noBindingMsgSuppressed) { - hint(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "Binding for path " + path + " has no source, so can't be checked"); - } - } - } - } catch (Exception e) { - rule(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "Error " + e.getMessage() + " validating Coding: " + e.toString()); - } - } - } - - private CodeableConcept convertToCodeableConcept(Element element, StructureDefinition logical) { - CodeableConcept res = new CodeableConcept(); - for (ElementDefinition ed : logical.getSnapshot().getElement()) { - if (Utilities.charCount(ed.getPath(), '.') == 1) { - List maps = getMapping("http://hl7.org/fhir/terminology-pattern", logical, ed); - for (String m : maps) { - String name = tail(ed.getPath()); - List list = new ArrayList<>(); - element.getNamedChildren(name, list); - if (!list.isEmpty()) { - if ("Coding.code".equals(m)) { - res.getCodingFirstRep().setCode(list.get(0).primitiveValue()); - } else if ("Coding.system[fmt:OID]".equals(m)) { - String oid = list.get(0).primitiveValue(); - String url = context.oid2Uri(oid); - if (url != null) { - res.getCodingFirstRep().setSystem(url); - } else { - res.getCodingFirstRep().setSystem("urn:oid:" + oid); - } - } else if ("Coding.version".equals(m)) { - res.getCodingFirstRep().setVersion(list.get(0).primitiveValue()); - } else if ("Coding.display".equals(m)) { - res.getCodingFirstRep().setDisplay(list.get(0).primitiveValue()); - } else if ("CodeableConcept.text".equals(m)) { - res.setText(list.get(0).primitiveValue()); - } else if ("CodeableConcept.coding".equals(m)) { - StructureDefinition c = context.fetchTypeDefinition(ed.getTypeFirstRep().getCode()); - for (Element e : list) { - res.addCoding(convertToCoding(e, c)); - } - } - } - } - } - } - return res; - } - - private Coding convertToCoding(Element element, StructureDefinition logical) { - Coding res = new Coding(); - for (ElementDefinition ed : logical.getSnapshot().getElement()) { - if (Utilities.charCount(ed.getPath(), '.') == 1) { - List maps = getMapping("http://hl7.org/fhir/terminology-pattern", logical, ed); - for (String m : maps) { - String name = tail(ed.getPath()); - List list = new ArrayList<>(); - element.getNamedChildren(name, list); - if (!list.isEmpty()) { - if ("Coding.code".equals(m)) { - res.setCode(list.get(0).primitiveValue()); - } else if ("Coding.system[fmt:OID]".equals(m)) { - String oid = list.get(0).primitiveValue(); - String url = context.oid2Uri(oid); - if (url != null) { - res.setSystem(url); - } else { - res.setSystem("urn:oid:" + oid); - } - } else if ("Coding.version".equals(m)) { - res.setVersion(list.get(0).primitiveValue()); - } else if ("Coding.display".equals(m)) { - res.setDisplay(list.get(0).primitiveValue()); - } - } - } - } - } - return res; - } - - private void checkMaxValueSet(List errors, String path, Element element, StructureDefinition profile, String maxVSUrl, CodeableConcept cc, NodeStack stack) { - // TODO Auto-generated method stub - ValueSet valueset = resolveBindingReference(profile, maxVSUrl, profile.getUrl()); - if (warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, valueset != null, "ValueSet " + describeReference(maxVSUrl) + " not found by validator")) { - try { - long t = System.nanoTime(); - ValidationResult vr = context.validateCode(new ValidationOptions(stack.workingLang), cc, valueset); - txTime = txTime + (System.nanoTime() - t); - if (!vr.isOk()) { - if (vr.getErrorClass() != null && vr.getErrorClass().isInfrastructure()) - txWarning(errors, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false, "None of the codes provided could be validated against the maximum value set " + describeReference(maxVSUrl) + " (" + valueset.getUrl() + "), (error = " + vr.getMessage() + ")"); - else - txRule(errors, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false, "None of the codes provided are in the maximum value set " + describeReference(maxVSUrl) + " (" + valueset.getUrl() + ", and a code from this value set is required) (codes = " + ccSummary(cc) + ")"); - } - } catch (Exception e) { - warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "Error " + e.getMessage() + " validating CodeableConcept using maxValueSet"); - } - } - } - - private void checkMaxValueSet(List errors, String path, Element element, StructureDefinition profile, String maxVSUrl, Coding c, NodeStack stack) { - // TODO Auto-generated method stub - ValueSet valueset = resolveBindingReference(profile, maxVSUrl, profile.getUrl()); - if (warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, valueset != null, "ValueSet " + describeReference(maxVSUrl) + " not found by validator")) { - try { - long t = System.nanoTime(); - ValidationResult vr = context.validateCode(new ValidationOptions(stack.workingLang), c, valueset); - txTime = txTime + (System.nanoTime() - t); - if (!vr.isOk()) { - if (vr.getErrorClass() != null && vr.getErrorClass().isInfrastructure()) - txWarning(errors, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false, "The code provided could not be validated against the maximum value set " + describeReference(maxVSUrl) + " (" + valueset.getUrl() + "), (error = " + vr.getMessage() + ")"); - else - txRule(errors, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false, "The code provided is not in the maximum value set " + describeReference(maxVSUrl) + " (" + valueset.getUrl() + ", and a code from this value set is required) (code = " + c.getSystem() + "#" + c.getCode() + ")"); - } - } catch (Exception e) { - warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "Error " + e.getMessage() + " validating CodeableConcept using maxValueSet"); - } - } - } - - private void checkMaxValueSet(List errors, String path, Element element, StructureDefinition profile, String maxVSUrl, String value, NodeStack stack) { - // TODO Auto-generated method stub - ValueSet valueset = resolveBindingReference(profile, maxVSUrl, profile.getUrl()); - if (warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, valueset != null, "ValueSet " + describeReference(maxVSUrl) + " not found by validator")) { - try { - long t = System.nanoTime(); - ValidationResult vr = context.validateCode(new ValidationOptions(stack.workingLang), value, valueset); - txTime = txTime + (System.nanoTime() - t); - if (!vr.isOk()) { - if (vr.getErrorClass() != null && vr.getErrorClass().isInfrastructure()) - txWarning(errors, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false, "The code provided could not be validated against the maximum value set " + describeReference(maxVSUrl) + " (" + valueset.getUrl() + "), (error = " + vr.getMessage() + ")"); - else - txRule(errors, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false, "The code provided is not in the maximum value set " + describeReference(maxVSUrl) + " (" + valueset.getUrl() + "), and a code from this value set is required) (code = " + value + "), (error = " + vr.getMessage() + ")"); - } - } catch (Exception e) { - warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "Error " + e.getMessage() + " validating CodeableConcept using maxValueSet"); - } - } - } - - private String ccSummary(CodeableConcept cc) { - CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder(); - for (Coding c : cc.getCoding()) - b.append(c.getSystem() + "#" + c.getCode()); - return b.toString(); - } - - private void checkCoding(List errors, String path, Element focus, Coding fixed, String fixedSource, boolean pattern) { - checkFixedValue(errors, path + ".system", focus.getNamedChild("system"), fixed.getSystemElement(), fixedSource, "system", focus, pattern); - checkFixedValue(errors, path + ".version", focus.getNamedChild("version"), fixed.getVersionElement(), fixedSource, "version", focus, pattern); - checkFixedValue(errors, path + ".code", focus.getNamedChild("code"), fixed.getCodeElement(), fixedSource, "code", focus, pattern); - checkFixedValue(errors, path + ".display", focus.getNamedChild("display"), fixed.getDisplayElement(), fixedSource, "display", focus, pattern); - checkFixedValue(errors, path + ".userSelected", focus.getNamedChild("userSelected"), fixed.getUserSelectedElement(), fixedSource, "userSelected", focus, pattern); - } - - private void checkCoding(List errors, String path, Element element, StructureDefinition profile, ElementDefinition theElementCntext, boolean inCodeableConcept, boolean checkDisplay, NodeStack stack) { - String code = element.getNamedChildValue("code"); - String system = element.getNamedChildValue("system"); - String display = element.getNamedChildValue("display"); - rule(errors, IssueType.CODEINVALID, element.line(), element.col(), path, isAbsolute(system), "Coding.system must be an absolute reference, not a local reference"); - - if (system != null && code != null && !noTerminologyChecks) { - rule(errors, IssueType.CODEINVALID, element.line(), element.col(), path, !isValueSet(system), "The Coding references a value set, not a code system ('" + system + "')"); - try { - if (checkCode(errors, element, path, code, system, display, checkDisplay, stack)) - if (theElementCntext != null && theElementCntext.hasBinding()) { - ElementDefinitionBindingComponent binding = theElementCntext.getBinding(); - if (warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, binding != null, "Binding for " + path + " missing")) { - if (binding.hasValueSet()) { - ValueSet valueset = resolveBindingReference(profile, binding.getValueSet(), profile.getUrl()); - if (warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, valueset != null, "ValueSet " + describeReference(binding.getValueSet()) + " not found by validator")) { - try { - Coding c = ObjectConverter.readAsCoding(element); - long t = System.nanoTime(); - ValidationResult vr = null; - if (binding.getStrength() != BindingStrength.EXAMPLE) { - vr = context.validateCode(new ValidationOptions(stack.workingLang), c, valueset); - } - txTime = txTime + (System.nanoTime() - t); - if (vr != null && !vr.isOk()) { - if (vr.IsNoService()) - txHint(errors, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false, "The value provided could not be validated in the absence of a terminology server"); - else if (vr.getErrorClass() != null && !vr.getErrorClass().isInfrastructure()) { - if (binding.getStrength() == BindingStrength.REQUIRED) - txWarning(errors, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false, "Could not confirm that the codes provided are in the value set " + describeReference(binding.getValueSet(), valueset) + ", and a code from this value set is required"); - else if (binding.getStrength() == BindingStrength.EXTENSIBLE) { - if (binding.hasExtension("http://hl7.org/fhir/StructureDefinition/elementdefinition-maxValueSet")) - checkMaxValueSet(errors, path, element, profile, ToolingExtensions.readStringExtension(binding, "http://hl7.org/fhir/StructureDefinition/elementdefinition-maxValueSet"), c, stack); - else if (!noExtensibleWarnings) - txWarning(errors, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false, "Could not confirm that the codes provided are in the value set " + describeReference(binding.getValueSet(), valueset) + ", and a code should come from this value set unless it has no suitable code"); - } else if (binding.getStrength() == BindingStrength.PREFERRED) { - if (baseOnly) { - txHint(errors, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false, "Could not confirm that the codes provided are in the value set " + describeReference(binding.getValueSet(), valueset) + ", and a code is recommended to come from this value set"); - } - } - } else if (binding.getStrength() == BindingStrength.REQUIRED) - txRule(errors, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false, "The Coding provided is not in the value set " + describeReference(binding.getValueSet(), valueset) + ", and a code is required from this value set. " + getErrorMessage(vr.getMessage())); - else if (binding.getStrength() == BindingStrength.EXTENSIBLE) { - if (binding.hasExtension("http://hl7.org/fhir/StructureDefinition/elementdefinition-maxValueSet")) - checkMaxValueSet(errors, path, element, profile, ToolingExtensions.readStringExtension(binding, "http://hl7.org/fhir/StructureDefinition/elementdefinition-maxValueSet"), c, stack); - else - txWarning(errors, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false, "The Coding provided is not in the value set " + describeReference(binding.getValueSet(), valueset) + ", and a code should come from this value set unless it has no suitable code. " + getErrorMessage(vr.getMessage())); - } else if (binding.getStrength() == BindingStrength.PREFERRED) { - if (baseOnly) { - txHint(errors, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false, "The Coding provided is not in the value set " + describeReference(binding.getValueSet(), valueset) + ", and a code is recommended to come from this value set. " + getErrorMessage(vr.getMessage())); - } - } - } - } catch (Exception e) { - warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "Error " + e.getMessage() + " validating Coding"); - } - } - } else if (binding.hasValueSet()) { - hint(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "Binding by URI reference cannot be checked"); - } else if (!inCodeableConcept && !noBindingMsgSuppressed) { - hint(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "Binding for path " + path + " has no source, so can't be checked"); - } - } - } - } catch (Exception e) { - rule(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "Error " + e.getMessage() + " validating Coding: " + e.toString()); - } - } - } - - private boolean isValueSet(String url) { - try { - ValueSet vs = context.fetchResourceWithException(ValueSet.class, url); - return vs != null; - } catch (Exception e) { - return false; - } - } - - private void checkContactPoint(List errors, String path, Element focus, ContactPoint fixed, String fixedSource, boolean pattern) { - checkFixedValue(errors, path + ".system", focus.getNamedChild("system"), fixed.getSystemElement(), fixedSource, "system", focus, pattern); - checkFixedValue(errors, path + ".value", focus.getNamedChild("value"), fixed.getValueElement(), fixedSource, "value", focus, pattern); - checkFixedValue(errors, path + ".use", focus.getNamedChild("use"), fixed.getUseElement(), fixedSource, "use", focus, pattern); - checkFixedValue(errors, path + ".period", focus.getNamedChild("period"), fixed.getPeriod(), fixedSource, "period", focus, pattern); - - } - - private StructureDefinition checkExtension(ValidatorHostContext hostContext, List errors, String path, Element resource, Element container, Element element, ElementDefinition def, StructureDefinition profile, NodeStack stack, NodeStack containerStack, String extensionUrl) throws FHIRException { - String url = element.getNamedChildValue("url"); - boolean isModifier = element.getName().equals("modifierExtension"); - - long t = System.nanoTime(); - StructureDefinition ex = Utilities.isAbsoluteUrl(url) ? context.fetchResource(StructureDefinition.class, url) : null; - sdTime = sdTime + (System.nanoTime() - t); - if (ex == null) { - if (xverManager == null) { - xverManager = new XVerExtensionManager(context); - } - if (xverManager.matchingUrl(url)) { - switch (xverManager.status(url)) { - case BadVersion: - rule(errors, IssueType.INVALID, element.line(), element.col(), path + "[url='" + url + "']", false, "Extension url '" + url + "' is not valid (invalidVersion '" + xverManager.getVersion(url) + "')"); - break; - case Unknown: - rule(errors, IssueType.INVALID, element.line(), element.col(), path + "[url='" + url + "']", false, "Extension url '" + url + "' is not valid (unknown Element id '" + xverManager.getElementId(url) + "')"); - break; - case Invalid: - rule(errors, IssueType.INVALID, element.line(), element.col(), path + "[url='" + url + "']", false, "Extension url '" + url + "' is not valid (Element id '" + xverManager.getElementId(url) + "' is valid, but cannot be used in a cross-version paradigm because there has been no changes across the relevant versions)"); - break; - case Valid: - ex = xverManager.makeDefinition(url); - context.generateSnapshot(ex); - context.cacheResource(ex); - break; - default: - rule(errors, IssueType.INVALID, element.line(), element.col(), path + "[url='" + url + "']", false, "Extension url '" + url + "' evaluation state illegal"); - break; - } - } else if (extensionUrl != null && !isAbsolute(url)) { - if (extensionUrl.equals(profile.getUrl())) { - rule(errors, IssueType.INVALID, element.line(), element.col(), path + "[url='" + url + "']", hasExtensionSlice(profile, url), "Sub-extension url '" + url + "' is not defined by the Extension " + profile.getUrl()); - } - } else if (rule(errors, IssueType.STRUCTURE, element.line(), element.col(), path, allowUnknownExtension(url), "The extension " + url + " is unknown, and not allowed here")) { - hint(errors, IssueType.STRUCTURE, element.line(), element.col(), path, isKnownExtension(url), "Unknown extension " + url); - } - } - if (ex != null) { - trackUsage(ex, hostContext, element); - if (def.getIsModifier()) { - rule(errors, IssueType.STRUCTURE, element.line(), element.col(), path + "[url='" + url + "']", ex.getSnapshot().getElement().get(0).getIsModifier(), - "Extension modifier mismatch: the extension element is labelled as a modifier, but the underlying extension is not"); - } else { - rule(errors, IssueType.STRUCTURE, element.line(), element.col(), path + "[url='" + url + "']", !ex.getSnapshot().getElement().get(0).getIsModifier(), - "Extension modifier mismatch: the extension element is not labelled as a modifier, but the underlying extension is"); - } - // two questions - // 1. can this extension be used here? - checkExtensionContext(errors, resource, container, ex, containerStack, hostContext); - - if (isModifier) - rule(errors, IssueType.STRUCTURE, element.line(), element.col(), path + "[url='" + url + "']", ex.getSnapshot().getElement().get(0).getIsModifier(), - "The Extension '" + url + "' must be used as a modifierExtension"); - else - rule(errors, IssueType.STRUCTURE, element.line(), element.col(), path + "[url='" + url + "']", !ex.getSnapshot().getElement().get(0).getIsModifier(), - "The Extension '" + url + "' must not be used as an extension (it's a modifierExtension)"); - - // check the type of the extension: - Set allowedTypes = listExtensionTypes(ex); - String actualType = getExtensionType(element); - if (actualType == null) - rule(errors, IssueType.STRUCTURE, element.line(), element.col(), path, allowedTypes.isEmpty(), "The Extension '" + url + "' definition is for a simple extension, so it must contain a value, not extensions"); - else - rule(errors, IssueType.STRUCTURE, element.line(), element.col(), path, allowedTypes.contains(actualType), "The Extension '" + url + "' definition allows for the types " + allowedTypes.toString() + " but found type " + actualType); - - // 3. is the content of the extension valid? - validateElement(hostContext, errors, ex, ex.getSnapshot().getElement().get(0), null, null, resource, element, "Extension", stack, false, true, url); - - } - return ex; - } - - private boolean hasExtensionSlice(StructureDefinition profile, String sliceName) { - for (ElementDefinition ed : profile.getSnapshot().getElement()) { - if (ed.getPath().equals("Extension.extension.url") && ed.hasFixed() && sliceName.equals(ed.getFixed().primitiveValue())) { - return true; - } - } - return false; - } - - private String getExtensionType(Element element) { - for (Element e : element.getChildren()) { - if (e.getName().startsWith("value")) { - String tn = e.getName().substring(5); - String ltn = Utilities.uncapitalize(tn); - if (isPrimitiveType(ltn)) - return ltn; - else - return tn; - } - } + public Base resolveConstant(Object appContext, String name, boolean beforeContext) throws PathEngineException { + ValidatorHostContext c = (ValidatorHostContext) appContext; + if (externalHostServices != null) + return externalHostServices.resolveConstant(c.getAppContext(), name, beforeContext); + else return null; } - private Set listExtensionTypes(StructureDefinition ex) { - ElementDefinition vd = null; - for (ElementDefinition ed : ex.getSnapshot().getElement()) { - if (ed.getPath().startsWith("Extension.value")) { - vd = ed; - break; - } - } - Set res = new HashSet(); - if (vd != null && !"0".equals(vd.getMax())) { - for (TypeRefComponent tr : vd.getType()) { - res.add(tr.getWorkingCode()); - } - } + @Override + public TypeDetails resolveConstantType(Object appContext, String name) throws PathEngineException { + ValidatorHostContext c = (ValidatorHostContext) appContext; + if (externalHostServices != null) + return externalHostServices.resolveConstantType(c.getAppContext(), name); + else + return null; + } + + @Override + public boolean log(String argument, List focus) { + if (externalHostServices != null) + return externalHostServices.log(argument, focus); + else + return false; + } + + @Override + public FunctionDetails resolveFunction(String functionName) { + throw new Error("Not done yet (ValidatorHostServices.resolveFunction): " + functionName); + } + + @Override + public TypeDetails checkFunction(Object appContext, String functionName, List parameters) throws PathEngineException { + throw new Error("Not done yet (ValidatorHostServices.checkFunction)"); + } + + @Override + public List executeFunction(Object appContext, String functionName, List> parameters) { + throw new Error("Not done yet (ValidatorHostServices.executeFunction)"); + } + + @Override + public Base resolveReference(Object appContext, String url, Base refContext) throws FHIRException { + ValidatorHostContext c = (ValidatorHostContext) appContext; + + if (refContext != null && refContext.hasUserData("validator.bundle.resolution")) { + return (Base) refContext.getUserData("validator.bundle.resolution"); + } + + if (c.getAppContext() instanceof Element) { + Element bnd = (Element) c.getAppContext(); + Base res = resolveInBundle(url, bnd); + if (res != null) + return res; + } + Base res = resolveInBundle(url, c.getResource()); + if (res != null) return res; + res = resolveInBundle(url, c.getContainer()); + if (res != null) + return res; + + if (externalHostServices != null) + return externalHostServices.resolveReference(c.getAppContext(), url, refContext); + else if (fetcher != null) + try { + return fetcher.fetch(c.getAppContext(), url); + } catch (IOException e) { + throw new FHIRException(e); + } + else + throw new Error("Not done yet - resolve " + url + " locally (2)"); + } - private boolean checkExtensionContext(List errors, Element resource, Element container, StructureDefinition definition, NodeStack stack, ValidatorHostContext hostContext) { - String extUrl = definition.getUrl(); - boolean ok = false; - CommaSeparatedStringBuilder contexts = new CommaSeparatedStringBuilder(); - List plist = new ArrayList<>(); - plist.add(stripIndexes(stack.getLiteralPath())); - for (String s : stack.getLogicalPaths()) { - String p = stripIndexes(s); - // all extensions are always allowed in ElementDefinition.example.value, and in fixed and pattern values. TODO: determine the logical paths from the path stated in the element definition.... - if (Utilities.existsInList(p, "ElementDefinition.example.value", "ElementDefinition.pattern", "ElementDefinition.fixed")) { - return true; - } - plist.add(p); - + 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("fullUrl"); + String rt = res.fhirType(); + String id = res.getChildValue("id"); + if (url.equals(fullUrl)) + return res; + if (url.equals(rt + "/" + id)) + return res; + } } + } + return null; + } - for (StructureDefinitionContextComponent ctxt : fixContexts(extUrl, definition.getContext())) { - if (ok) { - break; - } - if (ctxt.getType() == ExtensionContextType.ELEMENT) { - String en = ctxt.getExpression(); - contexts.append("e:" + en); - if ("Element".equals(en)) { - ok = true; - } else if (en.equals("Resource") && container.isResource()) { - ok = true; - } - for (String p : plist) { - if (ok) { - break; - } - if (p.equals(en)) { - ok = true; - } else { - String pn = p; - String pt = ""; - if (p.contains(".")) { - pn = p.substring(0, p.indexOf(".")); - pt = p.substring(p.indexOf(".")); - } - StructureDefinition sd = context.fetchTypeDefinition(pn); - while (sd != null) { - if ((sd.getType() + pt).equals(en)) { - ok = true; - break; - } - if (sd.getBaseDefinition() != null) { - sd = context.fetchResource(StructureDefinition.class, sd.getBaseDefinition()); - } else { - sd = null; - } - } - } - } - } else if (ctxt.getType() == ExtensionContextType.EXTENSION) { - contexts.append("x:" + ctxt.getExpression()); - NodeStack estack = stack.parent; - if (estack != null && estack.getElement().fhirType().equals("Extension")) { - String ext = estack.element.getNamedChildValue("url"); - if (ctxt.getExpression().equals(ext)) { - ok = true; - } - } - } else if (ctxt.getType() == ExtensionContextType.FHIRPATH) { - contexts.append("p:" + ctxt.getExpression()); - // The context is all elements that match the FHIRPath query found in the expression. - List res = fpe.evaluate(hostContext, resource, hostContext.getRootResource(), container, fpe.parse(ctxt.getExpression())); - if (res.contains(container)) { - ok = true; - } - } else { - throw new Error("Unrecognised extension context " + ctxt.getTypeElement().asStringValue()); - } + @Override + public boolean conformsToProfile(Object appContext, Base item, String url) throws FHIRException { + ValidatorHostContext ctxt = (ValidatorHostContext) appContext; + StructureDefinition sd = context.fetchResource(StructureDefinition.class, url); + if (sd == null) { + throw new FHIRException("Unable to resolve " + url); + } + InstanceValidator self = InstanceValidator.this; + List valerrors = new ArrayList(); + if (item instanceof Resource) { + try { + Element e = new ObjectConverter(context).convert((Resource) item); + self.validateResource(new ValidatorHostContext(ctxt.getAppContext(), e), valerrors, e, e, sd, IdStatus.OPTIONAL, new NodeStack(e)); + } catch (IOException e1) { + throw new FHIRException(e1); } - if (!ok) { - rule(errors, IssueType.STRUCTURE, container.line(), container.col(), stack.literalPath, false, "The extension " + extUrl + " is not allowed to be used at this point (allowed = " + contexts.toString() + "; this element is [" + plist.toString() + ")"); - return false; + } else if (item instanceof Element) { + Element e = (Element) item; + if (e.isResource()) { + self.validateResource(new ValidatorHostContext(ctxt.getAppContext(), e), valerrors, e, e, sd, IdStatus.OPTIONAL, new NodeStack(e)); } else { - if (definition.hasContextInvariant()) { - for (StringType s : definition.getContextInvariant()) { - if (!fpe.evaluateToBoolean(hostContext, resource, hostContext.getRootResource(), container, fpe.parse(s.getValue()))) { - rule(errors, IssueType.STRUCTURE, container.line(), container.col(), stack.literalPath, false, - "The extension " + extUrl + " is not allowed to be used at this point (based on context invariant '" + s.getValue() + "')"); - return false; - } - } - } - return true; + throw new FHIRException("Not supported yet"); } + } else + throw new NotImplementedException("Not done yet (ValidatorHostServices.conformsToProfile), when item is not an element"); + boolean ok = true; + List record = new ArrayList<>(); + for (ValidationMessage v : valerrors) { + ok = ok && !v.getLevel().isError(); + if (v.getLevel().isError() || v.isSlicingHint()) { + record.add(v); + } + } + if (!ok && !record.isEmpty()) { + ctxt.sliceNotes(url, record); + } + return ok; } - private List fixContexts(String extUrl, List list) { - List res = new ArrayList<>(); - for (StructureDefinitionContextComponent ctxt : list) { - res.add(ctxt.copy()); - } - if ("http://hl7.org/fhir/StructureDefinition/structuredefinition-fhir-type".equals(extUrl)) { - list.get(0).setExpression("ElementDefinition.type"); - } - if ("http://hl7.org/fhir/StructureDefinition/regex".equals(extUrl)) { - list.get(1).setExpression("ElementDefinition.type"); - } - return list; - } - - private String stripIndexes(String path) { - boolean skip = false; - StringBuilder b = new StringBuilder(); - for (char c : path.toCharArray()) { - if (skip) { - if (c == ']') { - skip = false; - } - } else if (c == '[') { - skip = true; - } else { - b.append(c); - } - } - return b.toString(); - } - - private void checkFixedValue(List errors, String path, Element focus, org.hl7.fhir.r5.model.Element fixed, String fixedSource, String propName, Element parent) { - checkFixedValue(errors, path, focus, fixed, fixedSource, propName, parent, false); - } - - @SuppressWarnings("rawtypes") - private void checkFixedValue(List errors, String path, Element focus, org.hl7.fhir.r5.model.Element fixed, String fixedSource, String propName, Element parent, boolean pattern) { - if ((fixed == null || fixed.isEmpty()) && focus == null) { - ; // this is all good - } else if ((fixed == null || fixed.isEmpty()) && focus != null) { - rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, pattern, "The element " + focus.getName() + " is present in the instance but not allowed in the applicable " + (pattern ? "pattern" : "fixed value") + " specified in profile"); - } else if (fixed != null && !fixed.isEmpty() && focus == null) { - rule(errors, IssueType.VALUE, parent == null ? -1 : parent.line(), parent == null ? -1 : parent.col(), path, false, "Missing element '" + propName + "' - required by fixed value assigned in profile " + fixedSource); - } else { - String value = focus.primitiveValue(); - if (fixed instanceof org.hl7.fhir.r5.model.BooleanType) - rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.r5.model.BooleanType) fixed).asStringValue(), value), - "Value is '" + value + "' but must be '" + ((org.hl7.fhir.r5.model.BooleanType) fixed).asStringValue() + "'"); - else if (fixed instanceof org.hl7.fhir.r5.model.IntegerType) - rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.r5.model.IntegerType) fixed).asStringValue(), value), - "Value is '" + value + "' but must be '" + ((org.hl7.fhir.r5.model.IntegerType) fixed).asStringValue() + "'"); - else if (fixed instanceof org.hl7.fhir.r5.model.DecimalType) - rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.r5.model.DecimalType) fixed).asStringValue(), value), - "Value is '" + value + "' but must be '" + ((org.hl7.fhir.r5.model.DecimalType) fixed).asStringValue() + "'"); - else if (fixed instanceof org.hl7.fhir.r5.model.Base64BinaryType) - rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.r5.model.Base64BinaryType) fixed).asStringValue(), value), - "Value is '" + value + "' but must be '" + ((org.hl7.fhir.r5.model.Base64BinaryType) fixed).asStringValue() + "'"); - else if (fixed instanceof org.hl7.fhir.r5.model.InstantType) - rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.r5.model.InstantType) fixed).getValue().toString(), value), - "Value is '" + value + "' but must be '" + ((org.hl7.fhir.r5.model.InstantType) fixed).asStringValue() + "'"); - else if (fixed instanceof org.hl7.fhir.r5.model.CodeType) - rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.r5.model.CodeType) fixed).getValue(), value), - "Value is '" + value + "' but must be '" + ((org.hl7.fhir.r5.model.CodeType) fixed).getValue() + "'"); - else if (fixed instanceof org.hl7.fhir.r5.model.Enumeration) - rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.r5.model.Enumeration) fixed).asStringValue(), value), - "Value is '" + value + "' but must be '" + ((org.hl7.fhir.r5.model.Enumeration) fixed).asStringValue() + "'"); - else if (fixed instanceof org.hl7.fhir.r5.model.StringType) - rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.r5.model.StringType) fixed).getValue(), value), - "Value is '" + value + "' but must be '" + ((org.hl7.fhir.r5.model.StringType) fixed).getValue() + "'"); - else if (fixed instanceof org.hl7.fhir.r5.model.UriType) - rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.r5.model.UriType) fixed).getValue(), value), - "Value is '" + value + "' but must be '" + ((org.hl7.fhir.r5.model.UriType) fixed).getValue() + "'"); - else if (fixed instanceof org.hl7.fhir.r5.model.DateType) - rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.r5.model.DateType) fixed).getValue().toString(), value), - "Value is '" + value + "' but must be '" + ((org.hl7.fhir.r5.model.DateType) fixed).getValue() + "'"); - else if (fixed instanceof org.hl7.fhir.r5.model.DateTimeType) - rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.r5.model.DateTimeType) fixed).getValue().toString(), value), - "Value is '" + value + "' but must be '" + ((org.hl7.fhir.r5.model.DateTimeType) fixed).getValue() + "'"); - else if (fixed instanceof org.hl7.fhir.r5.model.OidType) - rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.r5.model.OidType) fixed).getValue(), value), - "Value is '" + value + "' but must be '" + ((org.hl7.fhir.r5.model.OidType) fixed).getValue() + "'"); - else if (fixed instanceof org.hl7.fhir.r5.model.UuidType) - rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.r5.model.UuidType) fixed).getValue(), value), - "Value is '" + value + "' but must be '" + ((org.hl7.fhir.r5.model.UuidType) fixed).getValue() + "'"); - else if (fixed instanceof org.hl7.fhir.r5.model.IdType) - rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.r5.model.IdType) fixed).getValue(), value), - "Value is '" + value + "' but must be '" + ((org.hl7.fhir.r5.model.IdType) fixed).getValue() + "'"); - else if (fixed instanceof Quantity) - checkQuantity(errors, path, focus, (Quantity) fixed, fixedSource, pattern); - else if (fixed instanceof Address) - checkAddress(errors, path, focus, (Address) fixed, fixedSource, pattern); - else if (fixed instanceof ContactPoint) - checkContactPoint(errors, path, focus, (ContactPoint) fixed, fixedSource, pattern); - else if (fixed instanceof Attachment) - checkAttachment(errors, path, focus, (Attachment) fixed, fixedSource, pattern); - else if (fixed instanceof Identifier) - checkIdentifier(errors, path, focus, (Identifier) fixed, fixedSource, pattern); - else if (fixed instanceof Coding) - checkCoding(errors, path, focus, (Coding) fixed, fixedSource, pattern); - else if (fixed instanceof HumanName) - checkHumanName(errors, path, focus, (HumanName) fixed, fixedSource, pattern); - else if (fixed instanceof CodeableConcept) - checkCodeableConcept(errors, path, focus, (CodeableConcept) fixed, fixedSource, pattern); - else if (fixed instanceof Timing) - checkTiming(errors, path, focus, (Timing) fixed, fixedSource, pattern); - else if (fixed instanceof Period) - checkPeriod(errors, path, focus, (Period) fixed, fixedSource, pattern); - else if (fixed instanceof Range) - checkRange(errors, path, focus, (Range) fixed, fixedSource, pattern); - else if (fixed instanceof Ratio) - checkRatio(errors, path, focus, (Ratio) fixed, fixedSource, pattern); - else if (fixed instanceof SampledData) - checkSampledData(errors, path, focus, (SampledData) fixed, fixedSource, pattern); - + @Override + public ValueSet resolveValueSet(Object appContext, String url) { + ValidatorHostContext c = (ValidatorHostContext) appContext; + if (c.getProfile() != null && url.startsWith("#")) { + for (Resource r : c.getProfile().getContained()) { + if (r.getId().equals(url.substring(1))) { + if (r instanceof ValueSet) + return (ValueSet) r; else - rule(errors, IssueType.EXCEPTION, focus.line(), focus.col(), path, false, "Unhandled fixed value type " + fixed.getClass().getName()); - List extensions = new ArrayList(); - focus.getNamedChildren("extension", extensions); - if (fixed.getExtension().size() == 0) { - rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, extensions.size() == 0, "No extensions allowed, as the specified fixed value doesn't contain any extensions"); - } else if (rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, extensions.size() == fixed.getExtension().size(), - "Extensions count mismatch: expected " + Integer.toString(fixed.getExtension().size()) + " but found " + Integer.toString(extensions.size()))) { - for (Extension e : fixed.getExtension()) { - Element ex = getExtensionByUrl(extensions, e.getUrl()); - if (rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, ex != null, "Extension count mismatch: unable to find extension: " + e.getUrl())) { - checkFixedValue(errors, path, ex.getNamedChild("extension").getNamedChild("value"), e.getValue(), fixedSource, "extension.value", ex.getNamedChild("extension")); - } - } - } + throw new FHIRException("Reference " + url + " refers to a " + r.fhirType() + " not a ValueSet"); + } } + return null; + } + return context.fetchResource(ValueSet.class, url); } - private void checkHumanName(List errors, String path, Element focus, HumanName fixed, String fixedSource, boolean pattern) { - checkFixedValue(errors, path + ".use", focus.getNamedChild("use"), fixed.getUseElement(), fixedSource, "use", focus, pattern); - checkFixedValue(errors, path + ".text", focus.getNamedChild("text"), fixed.getTextElement(), fixedSource, "text", focus, pattern); - checkFixedValue(errors, path + ".period", focus.getNamedChild("period"), fixed.getPeriod(), fixedSource, "period", focus, pattern); + } - List parts = new ArrayList(); - focus.getNamedChildren("family", parts); - if (rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, parts.size() > 0 == fixed.hasFamily(), - "Expected " + (fixed.hasFamily() ? "1" : "0") + " but found " + Integer.toString(parts.size()) + " family elements")) { - for (int i = 0; i < parts.size(); i++) - checkFixedValue(errors, path + ".family", parts.get(i), fixed.getFamilyElement(), fixedSource, "family", focus, pattern); - } - focus.getNamedChildren("given", parts); - if (rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, parts.size() == fixed.getGiven().size(), - "Expected " + Integer.toString(fixed.getGiven().size()) + " but found " + Integer.toString(parts.size()) + " given elements")) { - for (int i = 0; i < parts.size(); i++) - checkFixedValue(errors, path + ".given", parts.get(i), fixed.getGiven().get(i), fixedSource, "given", focus, pattern); - } - focus.getNamedChildren("prefix", parts); - if (rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, parts.size() == fixed.getPrefix().size(), - "Expected " + Integer.toString(fixed.getPrefix().size()) + " but found " + Integer.toString(parts.size()) + " prefix elements")) { - for (int i = 0; i < parts.size(); i++) - checkFixedValue(errors, path + ".prefix", parts.get(i), fixed.getPrefix().get(i), fixedSource, "prefix", focus, pattern); - } - focus.getNamedChildren("suffix", parts); - if (rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, parts.size() == fixed.getSuffix().size(), - "Expected " + Integer.toString(fixed.getSuffix().size()) + " but found " + Integer.toString(parts.size()) + " suffix elements")) { - for (int i = 0; i < parts.size(); i++) - checkFixedValue(errors, path + ".suffix", parts.get(i), fixed.getSuffix().get(i), fixedSource, "suffix", focus, pattern); - } + private IWorkerContext context; + private FHIRPathEngine fpe; + + // configuration items + private CheckDisplayOption checkDisplay; + private boolean anyExtensionsAllowed; + private boolean errorForUnknownProfiles; + private boolean noInvariantChecks; + private boolean noTerminologyChecks; + private boolean hintAboutNonMustSupport; + private boolean showMessagesFromReferences; + private BestPracticeWarningLevel bpWarnings; + private String validationLanguage; + private boolean baseOnly; + + private List extensionDomains = new ArrayList(); + + private IdStatus resourceIdRule; + private boolean allowXsiLocation; + + // used during the build process to keep the overall volume of messages down + private boolean suppressLoincSnomedMessages; + + // time tracking + private long overall = 0; + private long txTime = 0; + private long sdTime = 0; + private long loadTime = 0; + private long fpeTime = 0; + + private boolean noBindingMsgSuppressed; + private boolean debug; + private Map fetchCache = new HashMap<>(); + private HashMap resourceTracker = new HashMap<>(); + private IValidatorResourceFetcher fetcher; + long time = 0; + private IEvaluationContext externalHostServices; + private boolean noExtensibleWarnings; + private String serverBase; + + private EnableWhenEvaluator myEnableWhenEvaluator = new EnableWhenEvaluator(); + private String executionId; + private XVerExtensionManager xverManager; + private IValidationProfileUsageTracker tracker; + private ValidatorHostServices validatorServices; + private boolean assumeValidRestReferences; + private boolean allowExamples; + private ResourceBundle messages = + ResourceBundle.getBundle("Messages", Locale.US); + + public InstanceValidator(IWorkerContext theContext, IEvaluationContext hostServices) { + super(); + this.context = theContext; + this.externalHostServices = hostServices; + fpe = new FHIRPathEngine(context); + validatorServices = new ValidatorHostServices(); + fpe.setHostServices(validatorServices); + if (theContext.getVersion().startsWith("3.0") || theContext.getVersion().startsWith("1.0")) + fpe.setLegacyMode(true); + source = Source.InstanceValidator; + } + + @Override + public boolean isNoExtensibleWarnings() { + return noExtensibleWarnings; + } + + @Override + public IResourceValidator setNoExtensibleWarnings(boolean noExtensibleWarnings) { + this.noExtensibleWarnings = noExtensibleWarnings; + return this; + } + + @Override + public boolean isShowMessagesFromReferences() { + return showMessagesFromReferences; + } + + @Override + public void setShowMessagesFromReferences(boolean showMessagesFromReferences) { + this.showMessagesFromReferences = showMessagesFromReferences; + } + + @Override + public boolean isNoInvariantChecks() { + return noInvariantChecks; + } + + @Override + public IResourceValidator setNoInvariantChecks(boolean value) { + this.noInvariantChecks = value; + return this; + } + + public IValidatorResourceFetcher getFetcher() { + return this.fetcher; + } + + public IResourceValidator setFetcher(IValidatorResourceFetcher value) { + this.fetcher = value; + return this; + } + + public IValidationProfileUsageTracker getTracker() { + return this.tracker; + } + + public IResourceValidator setTracker(IValidationProfileUsageTracker value) { + this.tracker = value; + return this; + } + + + public boolean isHintAboutNonMustSupport() { + return hintAboutNonMustSupport; + } + + public void setHintAboutNonMustSupport(boolean hintAboutNonMustSupport) { + this.hintAboutNonMustSupport = hintAboutNonMustSupport; + } + + public boolean isAssumeValidRestReferences() { + return this.assumeValidRestReferences; + } + + public void setAssumeValidRestReferences(boolean value) { + this.assumeValidRestReferences = value; + } + + public boolean isAllowExamples() { + return this.allowExamples; + } + + public void setAllowExamples(boolean value) { + this.allowExamples = value; + } + + + private boolean allowUnknownExtension(String url) { + if ((allowExamples && (url.contains("example.org") || url.contains("acme.com"))) || url.contains("nema.org") || url.startsWith("http://hl7.org/fhir/tools/StructureDefinition/") || url.equals("http://hl7.org/fhir/StructureDefinition/structuredefinition-expression")) + // Added structuredefinition-expression explicitly because it wasn't defined in the version of the spec it needs to be used with + return true; + for (String s : extensionDomains) + if (url.startsWith(s)) + return true; + return anyExtensionsAllowed; + } + + private boolean isKnownExtension(String url) { + // Added structuredefinition-expression and following extensions explicitly because they weren't defined in the version of the spec they need to be used with + if ((allowExamples && (url.contains("example.org") || url.contains("acme.com"))) || url.contains("nema.org") || url.startsWith("http://hl7.org/fhir/tools/StructureDefinition/") || url.equals("http://hl7.org/fhir/StructureDefinition/structuredefinition-expression") || url.equals(VersionConvertorConstants.IG_DEPENDSON_PACKAGE_EXTENSION)) + return true; + for (String s : extensionDomains) + if (url.startsWith(s)) + return true; + return false; + } + + private void bpCheck(List errors, IssueType invalid, int line, int col, String literalPath, boolean test, String message) { + if (bpWarnings != null) { + switch (bpWarnings) { + case Error: + rule(errors, invalid, line, col, literalPath, test, message); + break; + case Warning: + warning(errors, invalid, line, col, literalPath, test, message); + break; + case Hint: + hint(errors, invalid, line, col, literalPath, test, message); + break; + default: // do nothing + break; + } } + } - private void checkIdentifier(List errors, String path, Element element, ElementDefinition context) { - String system = element.getNamedChildValue("system"); - rule(errors, IssueType.CODEINVALID, element.line(), element.col(), path, isAbsolute(system), "Identifier.system must be an absolute reference, not a local reference"); + @Override + public org.hl7.fhir.r5.elementmodel.Element validate(Object appContext, List errors, InputStream stream, FhirFormat format) throws FHIRException { + return validate(appContext, errors, stream, format, new ArrayList<>()); + } + + @Override + public org.hl7.fhir.r5.elementmodel.Element validate(Object appContext, List errors, InputStream stream, FhirFormat format, String profile) throws FHIRException { + ArrayList profiles = new ArrayList<>(); + if (profile != null) { + profiles.add(getSpecifiedProfile(profile)); } + return validate(appContext, errors, stream, format, profiles); + } - private void checkIdentifier(List errors, String path, Element focus, Identifier fixed, String fixedSource, boolean pattern) { - checkFixedValue(errors, path + ".use", focus.getNamedChild("use"), fixed.getUseElement(), fixedSource, "use", focus, pattern); - checkFixedValue(errors, path + ".type", focus.getNamedChild("type"), fixed.getType(), fixedSource, "type", focus, pattern); - checkFixedValue(errors, path + ".system", focus.getNamedChild("system"), fixed.getSystemElement(), fixedSource, "system", focus, pattern); - checkFixedValue(errors, path + ".value", focus.getNamedChild("value"), fixed.getValueElement(), fixedSource, "value", focus, pattern); - checkFixedValue(errors, path + ".period", focus.getNamedChild("period"), fixed.getPeriod(), fixedSource, "period", focus, pattern); - checkFixedValue(errors, path + ".assigner", focus.getNamedChild("assigner"), fixed.getAssigner(), fixedSource, "assigner", focus, pattern); + private StructureDefinition getSpecifiedProfile(String profile) { + StructureDefinition sd = context.fetchResource(StructureDefinition.class, profile); + if (sd == null) { + throw new FHIRException("Unable to locate the profile '" + profile + "' in order to validate against it"); } + return sd; + } - private void checkPeriod(List errors, String path, Element focus, Period fixed, String fixedSource, boolean pattern) { - checkFixedValue(errors, path + ".start", focus.getNamedChild("start"), fixed.getStartElement(), fixedSource, "start", focus, pattern); - checkFixedValue(errors, path + ".end", focus.getNamedChild("end"), fixed.getEndElement(), fixedSource, "end", focus, pattern); + @Override + public org.hl7.fhir.r5.elementmodel.Element validate(Object appContext, List errors, InputStream stream, FhirFormat format, List profiles) throws FHIRException { + ParserBase parser = Manager.makeParser(context, format); + if (parser instanceof XmlParser) + ((XmlParser) parser).setAllowXsiLocation(allowXsiLocation); + parser.setupValidation(ValidationPolicy.EVERYTHING, errors); + long t = System.nanoTime(); + Element e; + try { + e = parser.parse(stream); + } catch (IOException e1) { + throw new FHIRException(e1); } + loadTime = System.nanoTime() - t; + if (e != null) + validate(appContext, errors, e, profiles); + return e; + } - private void checkPrimitive(Object appContext, List errors, String path, String type, ElementDefinition context, Element e, StructureDefinition profile, NodeStack node) throws FHIRException { - if (isBlank(e.primitiveValue())) { - if (e.primitiveValue() == null) - rule(errors, IssueType.INVALID, e.line(), e.col(), path, e.hasChildren(), "Primitive types must have a value or must have child extensions"); - else if (e.primitiveValue().length() == 0) - rule(errors, IssueType.INVALID, e.line(), e.col(), path, e.hasChildren(), "Primitive types must have a value that is not empty"); - else if (StringUtils.isWhitespace(e.primitiveValue())) - warning(errors, IssueType.INVALID, e.line(), e.col(), path, e.hasChildren(), "Primitive types should not only be whitespace"); - return; - } - String regex = context.getExtensionString(ToolingExtensions.EXT_REGEX); - if (regex != null) - rule(errors, IssueType.INVALID, e.line(), e.col(), path, e.primitiveValue().matches(regex), "Element value '" + e.primitiveValue() + "' does not meet regex '" + regex + "'"); + @Override + public org.hl7.fhir.r5.elementmodel.Element validate(Object appContext, List errors, Resource resource) throws FHIRException { + return validate(appContext, errors, resource, new ArrayList<>()); + } - if (type.equals("boolean")) { - rule(errors, IssueType.INVALID, e.line(), e.col(), path, "true".equals(e.primitiveValue()) || "false".equals(e.primitiveValue()), "boolean values must be 'true' or 'false'"); - } - if (type.equals("uri") || type.equals("oid") || type.equals("uuid") || type.equals("url") || type.equals("canonical")) { - String url = e.primitiveValue(); - rule(errors, IssueType.INVALID, e.line(), e.col(), path, !url.startsWith("oid:"), "URI values cannot start with oid:"); - rule(errors, IssueType.INVALID, e.line(), e.col(), path, !url.startsWith("uuid:"), "URI values cannot start with uuid:"); - rule(errors, IssueType.INVALID, e.line(), e.col(), path, url.equals(url.trim().replace(" ", "")) - // work around an old invalid example in a core package - || "http://www.acme.com/identifiers/patient or urn:ietf:rfc:3986 if the Identifier.value itself is a full uri".equals(url), "URI values cannot have whitespace('" + url + "')"); - rule(errors, IssueType.INVALID, e.line(), e.col(), path, !context.hasMaxLength() || context.getMaxLength() == 0 || url.length() <= context.getMaxLength(), "value is longer than permitted maximum length of " + context.getMaxLength()); - rule(errors, IssueType.INVALID, e.line(), e.col(), path, !context.hasMaxLength() || context.getMaxLength() == 0 || e.primitiveValue().length() <= context.getMaxLength(), "value is longer than permitted maximum length of " + context.getMaxLength()); - - if (type.equals("oid")) { - if (rule(errors, IssueType.INVALID, e.line(), e.col(), path, url.startsWith("urn:oid:"), "OIDs must start with urn:oid:")) - rule(errors, IssueType.INVALID, e.line(), e.col(), path, Utilities.isOid(url.substring(8)), "OIDs must be valid"); - } - if (type.equals("uuid")) { - rule(errors, IssueType.INVALID, e.line(), e.col(), path, url.startsWith("urn:uuid:"), "UUIDs must start with urn:uuid:"); - try { - UUID.fromString(url.substring(8)); - } catch (Exception ex) { - rule(errors, IssueType.INVALID, e.line(), e.col(), path, false, "UUIDs must be valid (" + ex.getMessage() + ")"); - } - } - - // now, do we check the URI target? - if (fetcher != null) { - boolean found; - try { - found = isDefinitionURL(url) || (allowExamples && (url.contains("example.org") || url.contains("acme.com"))) || (url.startsWith("http://hl7.org/fhir/tools")) || fetcher.resolveURL(appContext, path, url); - } catch (IOException e1) { - found = false; - } - rule(errors, IssueType.INVALID, e.line(), e.col(), path, found, "URL value '" + url + "' does not resolve"); - } - } - if (type.equals("id")) { - // work around an old issue with ElementDefinition.id - if (!context.getPath().equals("ElementDefinition.id") && !VersionUtilities.versionsCompatible("1.4", this.context.getVersion())) { - rule(errors, IssueType.INVALID, e.line(), e.col(), path, FormatUtilities.isValidId(e.primitiveValue()), "id value '" + e.primitiveValue() + "' is not valid"); - } - } - if (type.equalsIgnoreCase("string") && e.hasPrimitiveValue()) { - if (rule(errors, IssueType.INVALID, e.line(), e.col(), path, e.primitiveValue() == null || e.primitiveValue().length() > 0, "@value cannot be empty")) { - warning(errors, IssueType.INVALID, e.line(), e.col(), path, e.primitiveValue() == null || e.primitiveValue().trim().equals(e.primitiveValue()), "value should not start or finish with whitespace"); - if (rule(errors, IssueType.INVALID, e.line(), e.col(), path, e.primitiveValue().length() <= 1048576, "value is longer than permitted maximum length of 1 MB (1048576 bytes)")) { - rule(errors, IssueType.INVALID, e.line(), e.col(), path, !context.hasMaxLength() || context.getMaxLength() == 0 || e.primitiveValue().length() <= context.getMaxLength(), "value is longer than permitted maximum length of " + context.getMaxLength()); - } - } - } - if (type.equals("dateTime")) { - warning(errors, IssueType.INVALID, e.line(), e.col(), path, yearIsValid(e.primitiveValue()), "The value '" + e.primitiveValue() + "' is outside the range of reasonable years - check for data entry error"); - rule(errors, IssueType.INVALID, e.line(), e.col(), path, - e.primitiveValue() - .matches("([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)(-(0[1-9]|1[0-2])(-(0[1-9]|[1-2][0-9]|3[0-1])(T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]+)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00))?)?)?)?"), - "Not a valid date time"); - rule(errors, IssueType.INVALID, e.line(), e.col(), path, !hasTime(e.primitiveValue()) || hasTimeZone(e.primitiveValue()), "if a date has a time, it must have a timezone"); - rule(errors, IssueType.INVALID, e.line(), e.col(), path, !context.hasMaxLength() || context.getMaxLength() == 0 || e.primitiveValue().length() <= context.getMaxLength(), "value is longer than permitted maximum length of " + context.getMaxLength()); - try { - DateTimeType dt = new DateTimeType(e.primitiveValue()); - } catch (Exception ex) { - rule(errors, IssueType.INVALID, e.line(), e.col(), path, false, "Not a valid date/time (" + ex.getMessage() + ")"); - } - } - if (type.equals("time")) { - rule(errors, IssueType.INVALID, e.line(), e.col(), path, - e.primitiveValue() - .matches("([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)"), - "Not a valid time"); - try { - TimeType dt = new TimeType(e.primitiveValue()); - } catch (Exception ex) { - rule(errors, IssueType.INVALID, e.line(), e.col(), path, false, "Not a valid time (" + ex.getMessage() + ")"); - } - } - if (type.equals("date")) { - warning(errors, IssueType.INVALID, e.line(), e.col(), path, yearIsValid(e.primitiveValue()), "The value '" + e.primitiveValue() + "' is outside the range of reasonable years - check for data entry error"); - rule(errors, IssueType.INVALID, e.line(), e.col(), path, e.primitiveValue().matches("([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)(-(0[1-9]|1[0-2])(-(0[1-9]|[1-2][0-9]|3[0-1]))?)?"), - "Not a valid date"); - rule(errors, IssueType.INVALID, e.line(), e.col(), path, !context.hasMaxLength() || context.getMaxLength() == 0 || e.primitiveValue().length() <= context.getMaxLength(), "value is longer than permitted maximum value of " + context.getMaxLength()); - try { - DateType dt = new DateType(e.primitiveValue()); - } catch (Exception ex) { - rule(errors, IssueType.INVALID, e.line(), e.col(), path, false, "Not a valid date (" + ex.getMessage() + ")"); - } - } - if (type.equals("base64Binary")) { - String encoded = e.primitiveValue(); - if (isNotBlank(encoded)) { - /* - * Technically this is not bulletproof as some invalid base64 won't be caught, - * but I think it's good enough. The original code used Java8 Base64 decoder - * but I've replaced it with a regex for 2 reasons: - * 1. This code will run on any version of Java - * 2. This code doesn't actually decode, which is much easier on memory use for big payloads - */ - int charCount = 0; - for (int i = 0; i < encoded.length(); i++) { - char nextChar = encoded.charAt(i); - if (Character.isWhitespace(nextChar)) { - continue; - } - if (Character.isLetterOrDigit(nextChar)) { - charCount++; - } - if (nextChar == '/' || nextChar == '=' || nextChar == '+') { - charCount++; - } - } - - if (charCount > 0 && charCount % 4 != 0) { - String value = encoded.length() < 100 ? encoded : "(snip)"; - rule(errors, IssueType.INVALID, e.line(), e.col(), path, false, "The value '{0}' is not a valid Base64 value", value); - } - } - } - if (type.equals("integer") || type.equals("unsignedInt") || type.equals("positiveInt")) { - if (rule(errors, IssueType.INVALID, e.line(), e.col(), path, Utilities.isInteger(e.primitiveValue()), "The value '" + e.primitiveValue() + "' is not a valid integer")) { - Integer v = new Integer(e.getValue()).intValue(); - rule(errors, IssueType.INVALID, e.line(), e.col(), path, !context.hasMaxValueIntegerType() || !context.getMaxValueIntegerType().hasValue() || (context.getMaxValueIntegerType().getValue() >= v), "value is greater than permitted maximum value of " + (context.hasMaxValueIntegerType() ? context.getMaxValueIntegerType() : "")); - rule(errors, IssueType.INVALID, e.line(), e.col(), path, !context.hasMinValueIntegerType() || !context.getMinValueIntegerType().hasValue() || (context.getMinValueIntegerType().getValue() <= v), "value is less than permitted minimum value of " + (context.hasMinValueIntegerType() ? context.getMinValueIntegerType() : "")); - if (type.equals("unsignedInt")) - rule(errors, IssueType.INVALID, e.line(), e.col(), path, v >= 0, "value is less than permitted minimum value of 0"); - if (type.equals("positiveInt")) - rule(errors, IssueType.INVALID, e.line(), e.col(), path, v > 0, "value is less than permitted minimum value of 1"); - } - } - if (type.equals("integer64")) { - if (rule(errors, IssueType.INVALID, e.line(), e.col(), path, Utilities.isLong(e.primitiveValue()), "The value '" + e.primitiveValue() + "' is not a valid integer64")) { - Long v = new Long(e.getValue()).longValue(); - rule(errors, IssueType.INVALID, e.line(), e.col(), path, !context.hasMaxValueInteger64Type() || !context.getMaxValueInteger64Type().hasValue() || (context.getMaxValueInteger64Type().getValue() >= v), "value is greater than permitted maximum value of " + (context.hasMaxValueInteger64Type() ? context.getMaxValueInteger64Type() : "")); - rule(errors, IssueType.INVALID, e.line(), e.col(), path, !context.hasMinValueInteger64Type() || !context.getMinValueInteger64Type().hasValue() || (context.getMinValueInteger64Type().getValue() <= v), "value is less than permitted minimum value of " + (context.hasMinValueInteger64Type() ? context.getMinValueInteger64Type() : "")); - if (type.equals("unsignedInt")) - rule(errors, IssueType.INVALID, e.line(), e.col(), path, v >= 0, "value is less than permitted minimum value of 0"); - if (type.equals("positiveInt")) - rule(errors, IssueType.INVALID, e.line(), e.col(), path, v > 0, "value is less than permitted minimum value of 1"); - } - } - if (type.equals("decimal")) { - if (e.primitiveValue() != null) { - DecimalStatus ds = Utilities.checkDecimal(e.primitiveValue(), true, false); - if (rule(errors, IssueType.INVALID, e.line(), e.col(), path, ds == DecimalStatus.OK || ds == DecimalStatus.RANGE, "The value '" + e.primitiveValue() + "' is not a valid decimal")) - warning(errors, IssueType.VALUE, e.line(), e.col(), path, ds != DecimalStatus.RANGE, "The value '" + e.primitiveValue() + "' is outside the range of commonly/reasonably supported decimals"); - } - } - if (type.equals("instant")) { - rule(errors, IssueType.INVALID, e.line(), e.col(), path, - e.primitiveValue().matches("-?[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]+)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00))"), - "The instant '" + e.primitiveValue() + "' is not valid (by regex)"); - warning(errors, IssueType.INVALID, e.line(), e.col(), path, yearIsValid(e.primitiveValue()), "The value '" + e.primitiveValue() + "' is outside the range of reasonable years - check for data entry error"); - try { - InstantType dt = new InstantType(e.primitiveValue()); - } catch (Exception ex) { - rule(errors, IssueType.INVALID, e.line(), e.col(), path, false, "Not a valid instant (" + ex.getMessage() + ")"); - } - } - - if (type.equals("code") && e.primitiveValue() != null) { - // Technically, a code is restricted to string which has at least one character and no leading or trailing whitespace, and where there is no whitespace - // other than single spaces in the contents - rule(errors, IssueType.INVALID, e.line(), e.col(), path, passesCodeWhitespaceRules(e.primitiveValue()), "The code '" + e.primitiveValue() + "' is not valid (whitespace rules)"); - rule(errors, IssueType.INVALID, e.line(), e.col(), path, !context.hasMaxLength() || context.getMaxLength() == 0 || e.primitiveValue().length() <= context.getMaxLength(), "value is longer than permitted maximum length of " + context.getMaxLength()); - } - - if (context.hasBinding() && e.primitiveValue() != null) { - checkPrimitiveBinding(errors, path, type, context, e, profile, node); - } - - if (type.equals("xhtml")) { - XhtmlNode xhtml = e.getXhtml(); - if (xhtml != null) { // if it is null, this is an error already noted in the parsers - // check that the namespace is there and correct. - String ns = xhtml.getNsDecl(); - rule(errors, IssueType.INVALID, e.line(), e.col(), path, FormatUtilities.XHTML_NS.equals(ns), "Wrong namespace on the XHTML ('" + ns + "', should be '" + FormatUtilities.XHTML_NS + "')"); - // check that inner namespaces are all correct - checkInnerNS(errors, e, path, xhtml.getChildNodes()); - rule(errors, IssueType.INVALID, e.line(), e.col(), path, "div".equals(xhtml.getName()), "Wrong name on the XHTML ('" + ns + "') - must start with div"); - // check that no illegal elements and attributes have been used - checkInnerNames(errors, e, path, xhtml.getChildNodes()); - } - } - - if (context.hasFixed()) { - checkFixedValue(errors, path, e, context.getFixed(), profile.getUrl(), context.getSliceName(), null, false); - } - if (context.hasPattern()) { - checkFixedValue(errors, path, e, context.getPattern(), profile.getUrl(), context.getSliceName(), null, true); - } - - // for nothing to check + @Override + public org.hl7.fhir.r5.elementmodel.Element validate(Object appContext, List errors, Resource resource, String profile) throws FHIRException { + ArrayList profiles = new ArrayList<>(); + if (profile != null) { + profiles.add(getSpecifiedProfile(profile)); } + return validate(appContext, errors, resource, profiles); + } - private boolean isDefinitionURL(String url) { - return Utilities.existsInList(url, "http://hl7.org/fhirpath/System.Boolean", "http://hl7.org/fhirpath/System.String", "http://hl7.org/fhirpath/System.Integer", - "http://hl7.org/fhirpath/System.Decimal", "http://hl7.org/fhirpath/System.Date", "http://hl7.org/fhirpath/System.Time", "http://hl7.org/fhirpath/System.DateTime", "http://hl7.org/fhirpath/System.Quantity"); + @Override + public org.hl7.fhir.r5.elementmodel.Element validate(Object appContext, List errors, Resource resource, List profiles) throws FHIRException { + long t = System.nanoTime(); + Element e; + try { + e = new ObjectConverter(context).convert(resource); + } catch (IOException e1) { + throw new FHIRException(e1); } + loadTime = System.nanoTime() - t; + validate(appContext, errors, e, profiles); + return e; + } - private void checkInnerNames(List errors, Element e, String path, List list) { - for (XhtmlNode node : list) { - if (node.getNodeType() == NodeType.Element) { - rule(errors, IssueType.INVALID, e.line(), e.col(), path, Utilities.existsInList(node.getName(), - "p", "br", "div", "h1", "h2", "h3", "h4", "h5", "h6", "a", "span", "b", "em", "i", "strong", - "small", "big", "tt", "small", "dfn", "q", "var", "abbr", "acronym", "cite", "blockquote", "hr", "address", "bdo", "kbd", "q", "sub", "sup", - "ul", "ol", "li", "dl", "dt", "dd", "pre", "table", "caption", "colgroup", "col", "thead", "tr", "tfoot", "tbody", "th", "td", - "code", "samp", "img", "map", "area" + @Override + public org.hl7.fhir.r5.elementmodel.Element validate(Object appContext, List errors, org.w3c.dom.Element element) throws FHIRException { + return validate(appContext, errors, element, new ArrayList<>()); + } - ), "Illegal element name in the XHTML ('" + node.getName() + "')"); - for (String an : node.getAttributes().keySet()) { - boolean ok = an.startsWith("xmlns") || Utilities.existsInList(an, - "title", "style", "class", "id", "lang", "xml:lang", "dir", "accesskey", "tabindex", - // tables - "span", "width", "align", "valign", "char", "charoff", "abbr", "axis", "headers", "scope", "rowspan", "colspan") || + @Override + public org.hl7.fhir.r5.elementmodel.Element validate(Object appContext, List errors, org.w3c.dom.Element element, String profile) throws FHIRException { + ArrayList profiles = new ArrayList<>(); + if (profile != null) { + profiles.add(getSpecifiedProfile(profile)); + } + return validate(appContext, errors, element, profiles); + } - Utilities.existsInList(node.getName() + "." + an, "a.href", "a.name", "img.src", "img.border", "div.xmlns", "blockquote.cite", "q.cite", - "a.charset", "a.type", "a.name", "a.href", "a.hreflang", "a.rel", "a.rev", "a.shape", "a.coords", "img.src", - "img.alt", "img.longdesc", "img.height", "img.width", "img.usemap", "img.ismap", "map.name", "area.shape", - "area.coords", "area.href", "area.nohref", "area.alt", "table.summary", "table.width", "table.border", - "table.frame", "table.rules", "table.cellspacing", "table.cellpadding", "pre.space", "td.nowrap" - ); - if (!ok) - rule(errors, IssueType.INVALID, e.line(), e.col(), path, false, "Illegal attribute name in the XHTML ('" + an + "' on '" + node.getName() + "')"); - } - checkInnerNames(errors, e, path, node.getChildNodes()); - } + @Override + public org.hl7.fhir.r5.elementmodel.Element validate(Object appContext, List errors, org.w3c.dom.Element element, List profiles) throws FHIRException { + XmlParser parser = new XmlParser(context); + parser.setupValidation(ValidationPolicy.EVERYTHING, errors); + long t = System.nanoTime(); + Element e; + try { + e = parser.parse(element); + } catch (IOException e1) { + throw new FHIRException(e1); + } + loadTime = System.nanoTime() - t; + if (e != null) + validate(appContext, errors, e, profiles); + return e; + } + + @Override + public org.hl7.fhir.r5.elementmodel.Element validate(Object appContext, List errors, Document document) throws FHIRException { + return validate(appContext, errors, document, new ArrayList<>()); + } + + @Override + public org.hl7.fhir.r5.elementmodel.Element validate(Object appContext, List errors, Document document, String profile) throws FHIRException { + ArrayList profiles = new ArrayList<>(); + if (profile != null) { + profiles.add(getSpecifiedProfile(profile)); + } + return validate(appContext, errors, document, profiles); + } + + @Override + public org.hl7.fhir.r5.elementmodel.Element validate(Object appContext, List errors, Document document, List profiles) throws FHIRException { + XmlParser parser = new XmlParser(context); + parser.setupValidation(ValidationPolicy.EVERYTHING, errors); + long t = System.nanoTime(); + Element e; + try { + e = parser.parse(document); + } catch (IOException e1) { + throw new FHIRException(e1); + } + loadTime = System.nanoTime() - t; + if (e != null) + validate(appContext, errors, e, profiles); + return e; + } + + @Override + public org.hl7.fhir.r5.elementmodel.Element validate(Object appContext, List errors, JsonObject object) throws FHIRException { + return validate(appContext, errors, object, new ArrayList<>()); + } + + @Override + public org.hl7.fhir.r5.elementmodel.Element validate(Object appContext, List errors, JsonObject object, String profile) throws FHIRException { + ArrayList profiles = new ArrayList<>(); + if (profile != null) { + profiles.add(getSpecifiedProfile(profile)); + } + return validate(appContext, errors, object, profiles); + } + + @Override + public org.hl7.fhir.r5.elementmodel.Element validate(Object appContext, List errors, JsonObject object, List profiles) throws FHIRException { + JsonParser parser = new JsonParser(context); + parser.setupValidation(ValidationPolicy.EVERYTHING, errors); + long t = System.nanoTime(); + Element e = parser.parse(object); + loadTime = System.nanoTime() - t; + if (e != null) + validate(appContext, errors, e, profiles); + return e; + } + + @Override + public void validate(Object appContext, List errors, Element element) throws FHIRException { + validate(appContext, errors, element, new ArrayList<>()); + } + + @Override + public void validate(Object appContext, List errors, Element element, String profile) throws FHIRException { + ArrayList profiles = new ArrayList<>(); + if (profile != null) { + profiles.add(getSpecifiedProfile(profile)); + } + validate(appContext, errors, element, profiles); + } + + @Override + public void validate(Object appContext, List errors, Element element, List profiles) throws FHIRException { + // this is the main entry point; all the other public entry points end up here coming here... + // so the first thing to do is to clear the internal state + fetchCache.clear(); + fetchCache.put(element.fhirType() + "/" + element.getIdBase(), element); + resourceTracker.clear(); + executionId = UUID.randomUUID().toString(); + baseOnly = profiles.isEmpty(); + + long t = System.nanoTime(); + if (profiles == null || profiles.isEmpty()) { + validateResource(new ValidatorHostContext(appContext, element), errors, element, element, null, resourceIdRule, new NodeStack(element)); + } else { + for (StructureDefinition defn : profiles) { + validateResource(new ValidatorHostContext(appContext, element), errors, element, element, defn, resourceIdRule, new NodeStack(element)); + } + } + if (hintAboutNonMustSupport) { + checkElementUsage(errors, element, new NodeStack(element)); + } + overall = System.nanoTime() - t; + } + + private void checkElementUsage(List errors, Element element, NodeStack stack) { + String elementUsage = element.getUserString("elementSupported"); + hint(errors, IssueType.INFORMATIONAL, element.line(), element.col(), stack.getLiteralPath(), elementUsage == null || elementUsage.equals("Y"),messages.getString("The_element__is_not_marked_as_mustSupport_in_the_profile__Consider_not_using_the_element_or_marking_the_element_as_mustSupport_in_the_profile"), element.getName(), element.getProperty().getStructure().getUrl()); + + if (element.hasChildren()) { + String prevName = ""; + int elementCount = 0; + for (Element ce : element.getChildren()) { + if (ce.getName().equals(prevName)) + elementCount++; + else { + elementCount = 1; + prevName = ce.getName(); } + checkElementUsage(errors, ce, stack.push(ce, elementCount, null, null)); + } } + } - private void checkInnerNS(List errors, Element e, String path, List list) { - for (XhtmlNode node : list) { - if (node.getNodeType() == NodeType.Element) { - String ns = node.getNsDecl(); - rule(errors, IssueType.INVALID, e.line(), e.col(), path, ns == null || FormatUtilities.XHTML_NS.equals(ns), "Wrong namespace on the XHTML ('" + ns + "', should be '" + FormatUtilities.XHTML_NS + "')"); - checkInnerNS(errors, e, path, node.getChildNodes()); - } + private boolean check(String v1, String v2) { + return v1 == null ? Utilities.noString(v1) : v1.equals(v2); + } + + private void checkAddress(List errors, String path, Element focus, Address fixed, String fixedSource, boolean pattern) { + checkFixedValue(errors, path + ".use", focus.getNamedChild("use"), fixed.getUseElement(), fixedSource, "use", focus, pattern); + checkFixedValue(errors, path + ".text", focus.getNamedChild("text"), fixed.getTextElement(), fixedSource, "text", focus, pattern); + checkFixedValue(errors, path + ".city", focus.getNamedChild("city"), fixed.getCityElement(), fixedSource, "city", focus, pattern); + checkFixedValue(errors, path + ".state", focus.getNamedChild("state"), fixed.getStateElement(), fixedSource, "state", focus, pattern); + checkFixedValue(errors, path + ".country", focus.getNamedChild("country"), fixed.getCountryElement(), fixedSource, "country", focus, pattern); + checkFixedValue(errors, path + ".zip", focus.getNamedChild("zip"), fixed.getPostalCodeElement(), fixedSource, "postalCode", focus, pattern); + + List lines = new ArrayList(); + focus.getNamedChildren("line", lines); + if (rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, lines.size() == fixed.getLine().size(),messages.getString("Expected__but_found__line_elements"), Integer.toString(fixed.getLine().size()), Integer.toString(lines.size()))) { + for (int i = 0; i < lines.size(); i++) + checkFixedValue(errors, path + ".coding", lines.get(i), fixed.getLine().get(i), fixedSource, "coding", focus, pattern); + } + } + + private void checkAttachment(List errors, String path, Element focus, Attachment fixed, String fixedSource, boolean pattern) { + checkFixedValue(errors, path + ".contentType", focus.getNamedChild("contentType"), fixed.getContentTypeElement(), fixedSource, "contentType", focus, pattern); + checkFixedValue(errors, path + ".language", focus.getNamedChild("language"), fixed.getLanguageElement(), fixedSource, "language", focus, pattern); + checkFixedValue(errors, path + ".data", focus.getNamedChild("data"), fixed.getDataElement(), fixedSource, "data", focus, pattern); + checkFixedValue(errors, path + ".url", focus.getNamedChild("url"), fixed.getUrlElement(), fixedSource, "url", focus, pattern); + checkFixedValue(errors, path + ".size", focus.getNamedChild("size"), fixed.getSizeElement(), fixedSource, "size", focus, pattern); + checkFixedValue(errors, path + ".hash", focus.getNamedChild("hash"), fixed.getHashElement(), fixedSource, "hash", focus, pattern); + checkFixedValue(errors, path + ".title", focus.getNamedChild("title"), fixed.getTitleElement(), fixedSource, "title", focus, pattern); + } + + // public API + private boolean checkCode(List errors, Element element, String path, String code, String system, String display, boolean checkDisplay, NodeStack stack) throws TerminologyServiceException { + long t = System.nanoTime(); + boolean ss = context.supportsSystem(system); + txTime = txTime + (System.nanoTime() - t); + if (ss) { + t = System.nanoTime(); + ValidationResult s = context.validateCode(new ValidationOptions(stack.workingLang), system, code, checkDisplay ? display : null); + txTime = txTime + (System.nanoTime() - t); + if (s == null) + return true; + if (s.isOk()) { + if (s.getMessage() != null) + txWarning(errors, s.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, s == null, s.getMessage()); + return true; + } + if (s.getErrorClass() != null && s.getErrorClass().isInfrastructure()) + txWarning(errors, s.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, s == null, s.getMessage()); + else if (s.getSeverity() == IssueSeverity.INFORMATION) + txHint(errors, s.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, s == null, s.getMessage()); + else if (s.getSeverity() == IssueSeverity.WARNING) + txWarning(errors, s.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, s == null, s.getMessage()); + else + return txRule(errors, s.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, s == null, s.getMessage() + " for '" + system + "#" + code + "'"); + return true; + } else if (system.startsWith("http://hl7.org/fhir")) { + if (Utilities.existsInList(system, "http://hl7.org/fhir/sid/icd-10", "http://hl7.org/fhir/sid/cvx", "http://hl7.org/fhir/sid/icd-10", "http://hl7.org/fhir/sid/icd-10-cm", "http://hl7.org/fhir/sid/icd-9", "http://hl7.org/fhir/sid/ndc", "http://hl7.org/fhir/sid/srt")) + return true; // else don't check these (for now) + else if (system.startsWith("http://hl7.org/fhir/test")) + return true; // we don't validate these + else { + CodeSystem cs = getCodeSystem(system); + if (rule(errors, IssueType.CODEINVALID, element.line(), element.col(), path, cs != null,messages.getString("Unknown_Code_System_"), system)) { + ConceptDefinitionComponent def = getCodeDefinition(cs, code); + if (warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, def != null,messages.getString("Unknown_Code_"), system, code)) + return warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, display == null || display.equals(def.getDisplay()),messages.getString("Display_should_be_"), def.getDisplay()); } - } - - private void checkPrimitiveBinding(List errors, String path, String type, ElementDefinition elementContext, Element element, StructureDefinition profile, NodeStack stack) { - // We ignore bindings that aren't on string, uri or code - if (!element.hasPrimitiveValue() || !("code".equals(type) || "string".equals(type) || "uri".equals(type) || "url".equals(type) || "canonical".equals(type))) { - return; + return false; + } + } else if (context.isNoTerminologyServer() && Utilities.existsInList(system, "http://loinc.org", "http://unitsofmeasure.org", "http://snomed.info/sct", "http://www.nlm.nih.gov/research/umls/rxnorm")) { + return true; // no checks in this case + } else if (startsWithButIsNot(system, "http://snomed.info/sct", "http://loinc.org", "http://unitsofmeasure.org", "http://www.nlm.nih.gov/research/umls/rxnorm")) { + rule(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false,messages.getString("Invalid_System_URI_"), system); + return false; + } else { + try { + if (context.fetchResourceWithException(ValueSet.class, system) != null) { + rule(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false,messages.getString("Invalid_System_URI___cannot_use_a_value_set_URI_as_a_system"), system); + // Lloyd: This error used to prohibit checking for downstream issues, but there are some cases where that checking needs to occur. Please talk to me before changing the code back. } - if (noTerminologyChecks) - return; - - String value = element.primitiveValue(); - // System.out.println("check "+value+" in "+path); - - // firstly, resolve the value set - ElementDefinitionBindingComponent binding = elementContext.getBinding(); - if (binding.hasValueSet()) { - ValueSet vs = resolveBindingReference(profile, binding.getValueSet(), profile.getUrl()); - if (warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, vs != null, "ValueSet {0} not found by validator", describeReference(binding.getValueSet()))) { - long t = System.nanoTime(); - ValidationResult vr = null; - if (binding.getStrength() != BindingStrength.EXAMPLE) { - vr = context.validateCode(new ValidationOptions(stack.workingLang), value, vs); - } - txTime = txTime + (System.nanoTime() - t); - if (vr != null && !vr.isOk()) { - if (vr.IsNoService()) - txHint(errors, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false, "The value provided ('" + value + "') could not be validated in the absence of a terminology server"); - else if (binding.getStrength() == BindingStrength.REQUIRED) - txRule(errors, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false, "The value provided ('" + value + "') is not in the value set " + describeReference(binding.getValueSet()) + " (" + vs.getUrl() + ", and a code is required from this value set)" + getErrorMessage(vr.getMessage())); - else if (binding.getStrength() == BindingStrength.EXTENSIBLE) { - if (binding.hasExtension("http://hl7.org/fhir/StructureDefinition/elementdefinition-maxValueSet")) - checkMaxValueSet(errors, path, element, profile, ToolingExtensions.readStringExtension(binding, "http://hl7.org/fhir/StructureDefinition/elementdefinition-maxValueSet"), value, stack); - else if (!noExtensibleWarnings) - txWarning(errors, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false, "The value provided ('" + value + "') is not in the value set " + describeReference(binding.getValueSet()) + " (" + vs.getUrl() + ", and a code should come from this value set unless it has no suitable code)" + getErrorMessage(vr.getMessage())); - } else if (binding.getStrength() == BindingStrength.PREFERRED) { - if (baseOnly) { - txHint(errors, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false, "The value provided ('" + value + "') is not in the value set " + describeReference(binding.getValueSet()) + " (" + vs.getUrl() + ", and a code is recommended to come from this value set)" + getErrorMessage(vr.getMessage())); - } - } - } - } - } else if (!noBindingMsgSuppressed) - hint(errors, IssueType.CODEINVALID, element.line(), element.col(), path, !type.equals("code"), "Binding has no source, so can't be checked"); + hint(errors, IssueType.UNKNOWN, element.line(), element.col(), path, false,messages.getString("Code_System_URI__is_unknown_so_the_code_cannot_be_validated"), system); + return true; + } catch (Exception e) { + return true; + } } + } - private void checkQuantity(List errors, String path, Element focus, Quantity fixed, String fixedSource, boolean pattern) { - checkFixedValue(errors, path + ".value", focus.getNamedChild("value"), fixed.getValueElement(), fixedSource, "value", focus, pattern); - checkFixedValue(errors, path + ".comparator", focus.getNamedChild("comparator"), fixed.getComparatorElement(), fixedSource, "comparator", focus, pattern); - checkFixedValue(errors, path + ".units", focus.getNamedChild("unit"), fixed.getUnitElement(), fixedSource, "units", focus, pattern); - checkFixedValue(errors, path + ".system", focus.getNamedChild("system"), fixed.getSystemElement(), fixedSource, "system", focus, pattern); - checkFixedValue(errors, path + ".code", focus.getNamedChild("code"), fixed.getCodeElement(), fixedSource, "code", focus, pattern); - } + private boolean startsWithButIsNot(String system, String... uri) { + for (String s : uri) + if (!system.equals(s) && system.startsWith(s)) + return true; + return false; + } - // implementation - private void checkRange(List errors, String path, Element focus, Range fixed, String fixedSource, boolean pattern) { - checkFixedValue(errors, path + ".low", focus.getNamedChild("low"), fixed.getLow(), fixedSource, "low", focus, pattern); - checkFixedValue(errors, path + ".high", focus.getNamedChild("high"), fixed.getHigh(), fixedSource, "high", focus, pattern); - - } - - private void checkRatio(List errors, String path, Element focus, Ratio fixed, String fixedSource, boolean pattern) { - checkFixedValue(errors, path + ".numerator", focus.getNamedChild("numerator"), fixed.getNumerator(), fixedSource, "numerator", focus, pattern); - checkFixedValue(errors, path + ".denominator", focus.getNamedChild("denominator"), fixed.getDenominator(), fixedSource, "denominator", focus, pattern); - } - - private void checkReference(ValidatorHostContext hostContext, List errors, String path, Element element, StructureDefinition profile, ElementDefinition container, String parentType, NodeStack stack) throws FHIRException { - Reference reference = ObjectConverter.readAsReference(element); - - String ref = reference.getReference(); - if (Utilities.noString(ref)) { - if (Utilities.noString(reference.getIdentifier().getSystem()) && Utilities.noString(reference.getIdentifier().getValue())) { - warning(errors, IssueType.STRUCTURE, element.line(), element.col(), path, !Utilities.noString(element.getNamedChildValue("display")), "A Reference without an actual reference or identifier should have a display"); - } - return; - } else if (Utilities.existsInList(ref, "http://tools.ietf.org/html/bcp47")) { - // special known URLs that can't be validated but are known to be valid - return; + private boolean hasErrors(List errors) { + if (errors != null) { + for (ValidationMessage vm : errors) { + if (vm.getLevel() == IssueSeverity.FATAL || vm.getLevel() == IssueSeverity.ERROR) { + return true; } + } + } + return false; + } - ResolvedReference we = localResolve(ref, stack, errors, path, (Element) hostContext.getAppContext(), element); - String refType; - if (ref.startsWith("#")) { - refType = "contained"; - } else { - if (we == null) { - refType = "remote"; + private void checkCodeableConcept(List errors, String path, Element focus, CodeableConcept fixed, String fixedSource, boolean pattern) { + checkFixedValue(errors, path + ".text", focus.getNamedChild("text"), fixed.getTextElement(), fixedSource, "text", focus, pattern); + List codings = new ArrayList(); + focus.getNamedChildren("coding", codings); + if (pattern) { + if (rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, codings.size() >= fixed.getCoding().size(),messages.getString("Expected__but_found__coding_elements"), Integer.toString(fixed.getCoding().size()), Integer.toString(codings.size()))) { + for (int i = 0; i < fixed.getCoding().size(); i++) { + Coding fixedCoding = fixed.getCoding().get(i); + boolean found = false; + List allErrorsFixed = new ArrayList<>(); + List errorsFixed; + for (int j = 0; j < codings.size() && !found; ++j) { + errorsFixed = new ArrayList<>(); + checkFixedValue(errorsFixed, path + ".coding", codings.get(j), fixedCoding, fixedSource, "coding", focus, pattern); + if (!hasErrors(errorsFixed)) { + found = true; } else { - refType = "bundled"; + errorsFixed + .stream() + .filter(t -> t.getLevel().ordinal() >= IssueSeverity.ERROR.ordinal()) + .forEach(t -> allErrorsFixed.add(t)); } + } + if (!found) { + // The argonaut DSTU2 labs profile requires userSelected=false on the category.coding and this + // needs to produce an understandable error message + String message = "Expected CodeableConcept " + (pattern ? "pattern" : "fixed value") + " not found for" + + " system: " + fixedCoding.getSystemElement().asStringValue() + + " code: " + fixedCoding.getCodeElement().asStringValue() + + " display: " + fixedCoding.getDisplayElement().asStringValue(); + if (fixedCoding.hasUserSelected()) { + message += " userSelected: " + fixedCoding.getUserSelected(); + } + message += " - Issues: " + allErrorsFixed; + rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, false, message); + } } - ReferenceValidationPolicy pol = refType.equals("contained") || refType.equals("bundled") ? ReferenceValidationPolicy.CHECK_VALID : fetcher == null ? ReferenceValidationPolicy.IGNORE : fetcher.validationPolicy(hostContext.getAppContext(), path, ref); + } + } else { + if (rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, codings.size() == fixed.getCoding().size(),messages.getString("Expected__but_found__coding_elements"), Integer.toString(fixed.getCoding().size()), Integer.toString(codings.size()))) { + for (int i = 0; i < codings.size(); i++) + checkFixedValue(errors, path + ".coding", codings.get(i), fixed.getCoding().get(i), fixedSource, "coding", focus); + } + } + } - if (pol.checkExists()) { - if (we == null) { - if (fetcher == null) { - if (!refType.equals("contained")) - throw new FHIRException("Resource resolution services not provided"); - } else { - Element ext = null; - if (fetchCache.containsKey(ref)) { - ext = fetchCache.get(ref); - } else { - try { - ext = fetcher.fetch(hostContext.getAppContext(), ref); - } catch (IOException e) { - throw new FHIRException(e); - } - if (ext != null) { - fetchCache.put(ref, ext); - } - } - we = ext == null ? null : makeExternalRef(ext, path); + private boolean checkCodeableConcept(List errors, String path, Element element, StructureDefinition profile, ElementDefinition theElementCntext, NodeStack stack) { + boolean res = true; + if (!noTerminologyChecks && theElementCntext != null && theElementCntext.hasBinding()) { + ElementDefinitionBindingComponent binding = theElementCntext.getBinding(); + if (warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, binding != null,messages.getString("Binding_for__missing_cc"), path)) { + if (binding.hasValueSet()) { + ValueSet valueset = resolveBindingReference(profile, binding.getValueSet(), profile.getUrl()); + if (warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, valueset != null,messages.getString("ValueSet__not_found_by_validator"), describeReference(binding.getValueSet()))) { + try { + CodeableConcept cc = ObjectConverter.readAsCodeableConcept(element); + if (!cc.hasCoding()) { + if (binding.getStrength() == BindingStrength.REQUIRED) + rule(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "No code provided, and a code is required from the value set " + describeReference(binding.getValueSet()) + " (" + valueset.getUrl()); + else if (binding.getStrength() == BindingStrength.EXTENSIBLE) { + if (binding.hasExtension("http://hl7.org/fhir/StructureDefinition/elementdefinition-maxValueSet")) + rule(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false,messages.getString("No_code_provided_and_a_code_must_be_provided_from_the_value_set__max_value_set_"), describeReference(ToolingExtensions.readStringExtension(binding, "http://hl7.org/fhir/StructureDefinition/elementdefinition-maxValueSet")), valueset.getUrl()); + else + warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false,messages.getString("No_code_provided_and_a_code_should_be_provided_from_the_value_set__"), describeReference(binding.getValueSet()), valueset.getUrl()); } - } - rule(errors, IssueType.STRUCTURE, element.line(), element.col(), path, (allowExamples && (ref.contains("example.org") || ref.contains("acme.com"))) || (we != null || pol == ReferenceValidationPolicy.CHECK_TYPE_IF_EXISTS), "Unable to resolve resource '" + ref + "'"); - } + } else { + long t = System.nanoTime(); - String ft; - if (we != null) - ft = we.getType(); + // Check whether the codes are appropriate for the type of binding we have + boolean bindingsOk = true; + if (binding.getStrength() != BindingStrength.EXAMPLE) { + boolean atLeastOneSystemIsSupported = false; + for (Coding nextCoding : cc.getCoding()) { + String nextSystem = nextCoding.getSystem(); + if (isNotBlank(nextSystem) && context.supportsSystem(nextSystem)) { + atLeastOneSystemIsSupported = true; + break; + } + } + + if (!atLeastOneSystemIsSupported && binding.getStrength() == BindingStrength.EXAMPLE) { + // ignore this since we can't validate but it doesn't matter.. + } else { + ValidationResult vr = context.validateCode(new ValidationOptions(stack.workingLang).checkValueSetOnly(), cc, valueset); // we're going to validate the codings directly, so only check the valueset + if (!vr.isOk()) { + bindingsOk = false; + if (vr.getErrorClass() != null && vr.getErrorClass().isInfrastructure()) { + if (binding.getStrength() == BindingStrength.REQUIRED) + txWarning(errors, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false,messages.getString("Could_not_confirm_that_the_codes_provided_are_in_the_value_set__and_a_code_from_this_value_set_is_required_class__"), describeReference(binding.getValueSet()), vr.getErrorClass().toString()); + else if (binding.getStrength() == BindingStrength.EXTENSIBLE) { + if (binding.hasExtension("http://hl7.org/fhir/StructureDefinition/elementdefinition-maxValueSet")) + checkMaxValueSet(errors, path, element, profile, ToolingExtensions.readStringExtension(binding, "http://hl7.org/fhir/StructureDefinition/elementdefinition-maxValueSet"), cc, stack); + else if (!noExtensibleWarnings) + txWarning(errors, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false,messages.getString("Could_not_confirm_that_the_codes_provided_are_in_the_value_set__and_a_code_should_come_from_this_value_set_unless_it_has_no_suitable_code_class__"), describeReference(binding.getValueSet()), vr.getErrorClass().toString()); + } else if (binding.getStrength() == BindingStrength.PREFERRED) { + if (baseOnly) { + txHint(errors, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false,messages.getString("Could_not_confirm_that_the_codes_provided_are_in_the_value_set__and_a_code_is_recommended_to_come_from_this_value_set_class__"), describeReference(binding.getValueSet()), vr.getErrorClass().toString()); + } + } + } else { + if (binding.getStrength() == BindingStrength.REQUIRED) + txRule(errors, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false, "None of the codes provided are in the value set " + describeReference(binding.getValueSet()) + " (" + valueset.getUrl() + ", and a code from this value set is required) (codes = " + ccSummary(cc) + ")"); + else if (binding.getStrength() == BindingStrength.EXTENSIBLE) { + if (binding.hasExtension("http://hl7.org/fhir/StructureDefinition/elementdefinition-maxValueSet")) + checkMaxValueSet(errors, path, element, profile, ToolingExtensions.readStringExtension(binding, "http://hl7.org/fhir/StructureDefinition/elementdefinition-maxValueSet"), cc, stack); + if (!noExtensibleWarnings) + txWarning(errors, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false,messages.getString("None_of_the_codes_provided_are_in_the_value_set___and_a_code_should_come_from_this_value_set_unless_it_has_no_suitable_code_codes__"), describeReference(binding.getValueSet()), valueset.getUrl(), ccSummary(cc)); + } else if (binding.getStrength() == BindingStrength.PREFERRED) { + if (baseOnly) { + txHint(errors, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false,messages.getString("None_of_the_codes_provided_are_in_the_value_set___and_a_code_is_recommended_to_come_from_this_value_set_codes__"), describeReference(binding.getValueSet()), valueset.getUrl(), ccSummary(cc)); + } + } + } + } else if (vr.getMessage() != null) { + res = false; + txWarning(errors, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false, vr.getMessage()); + } else { + res = false; + } + } + // Then, for any codes that are in code systems we are able + // to validate, we'll validate that the codes actually exist + if (bindingsOk) { + for (Coding nextCoding : cc.getCoding()) { + if (isNotBlank(nextCoding.getCode()) && isNotBlank(nextCoding.getSystem()) && context.supportsSystem(nextCoding.getSystem())) { + ValidationResult vr = context.validateCode(new ValidationOptions(stack.workingLang).noCheckValueSetMembership(), nextCoding, valueset); + if (vr.getSeverity() != null) { + if (vr.getSeverity() == IssueSeverity.INFORMATION) { + txHint(errors, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false, vr.getMessage()); + } else if (vr.getSeverity() == IssueSeverity.WARNING) { + txWarning(errors, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false, vr.getMessage()); + } else { + txRule(errors, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false, vr.getMessage()); + } + } + } + } + } + txTime = txTime + (System.nanoTime() - t); + } + } + } catch (Exception e) { + warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false,messages.getString("Error__validating_CodeableConcept"), e.getMessage()); + } + } + } else if (binding.hasValueSet()) { + hint(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false,messages.getString("Binding_by_URI_reference_cannot_be_checked")); + } else if (!noBindingMsgSuppressed) { + hint(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false,messages.getString("Binding_for_path__has_no_source_so_cant_be_checked"), path); + } + } + } + return res; + } + + private boolean checkTerminologyCodeableConcept(List errors, String path, Element element, StructureDefinition profile, ElementDefinition theElementCntext, NodeStack stack, StructureDefinition logical) { + boolean res = true; + if (!noTerminologyChecks && theElementCntext != null && theElementCntext.hasBinding()) { + ElementDefinitionBindingComponent binding = theElementCntext.getBinding(); + if (warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, binding != null,messages.getString("Binding_for__missing_cc"), path)) { + if (binding.hasValueSet()) { + ValueSet valueset = resolveBindingReference(profile, binding.getValueSet(), profile.getUrl()); + if (warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, valueset != null,messages.getString("ValueSet__not_found_by_validator"), describeReference(binding.getValueSet()))) { + try { + CodeableConcept cc = convertToCodeableConcept(element, logical); + if (!cc.hasCoding()) { + if (binding.getStrength() == BindingStrength.REQUIRED) + rule(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false,messages.getString("No_code_provided_and_a_code_is_required_from_the_value_set__"), describeReference(binding.getValueSet()), valueset.getUrl()); + else if (binding.getStrength() == BindingStrength.EXTENSIBLE) { + if (binding.hasExtension("http://hl7.org/fhir/StructureDefinition/elementdefinition-maxValueSet")) + rule(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false,messages.getString("No_code_provided_and_a_code_must_be_provided_from_the_value_set__max_value_set_"), describeReference(ToolingExtensions.readStringExtension(binding, "http://hl7.org/fhir/StructureDefinition/elementdefinition-maxValueSet")), valueset.getUrl()); + else + warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false,messages.getString("No_code_provided_and_a_code_should_be_provided_from_the_value_set__"), describeReference(binding.getValueSet()), valueset.getUrl()); + } + } else { + long t = System.nanoTime(); + + // Check whether the codes are appropriate for the type of binding we have + boolean bindingsOk = true; + if (binding.getStrength() != BindingStrength.EXAMPLE) { + boolean atLeastOneSystemIsSupported = false; + for (Coding nextCoding : cc.getCoding()) { + String nextSystem = nextCoding.getSystem(); + if (isNotBlank(nextSystem) && context.supportsSystem(nextSystem)) { + atLeastOneSystemIsSupported = true; + break; + } + } + + if (!atLeastOneSystemIsSupported && binding.getStrength() == BindingStrength.EXAMPLE) { + // ignore this since we can't validate but it doesn't matter.. + } else { + ValidationResult vr = context.validateCode(new ValidationOptions(stack.workingLang), cc, valueset); // we're going to validate the codings directly + if (!vr.isOk()) { + bindingsOk = false; + if (vr.getErrorClass() != null && vr.getErrorClass().isInfrastructure()) { + if (binding.getStrength() == BindingStrength.REQUIRED) + txWarning(errors, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false,messages.getString("Could_not_confirm_that_the_codes_provided_are_in_the_value_set__and_a_code_from_this_value_set_is_required_class__"), describeReference(binding.getValueSet()), vr.getErrorClass().toString()); + else if (binding.getStrength() == BindingStrength.EXTENSIBLE) { + if (binding.hasExtension("http://hl7.org/fhir/StructureDefinition/elementdefinition-maxValueSet")) + checkMaxValueSet(errors, path, element, profile, ToolingExtensions.readStringExtension(binding, "http://hl7.org/fhir/StructureDefinition/elementdefinition-maxValueSet"), cc, stack); + else if (!noExtensibleWarnings) + txWarning(errors, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false,messages.getString("Could_not_confirm_that_the_codes_provided_are_in_the_value_set__and_a_code_should_come_from_this_value_set_unless_it_has_no_suitable_code_class__"), describeReference(binding.getValueSet()), vr.getErrorClass().toString()); + } else if (binding.getStrength() == BindingStrength.PREFERRED) { + if (baseOnly) { + txHint(errors, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false,messages.getString("Could_not_confirm_that_the_codes_provided_are_in_the_value_set__and_a_code_is_recommended_to_come_from_this_value_set_class__"), describeReference(binding.getValueSet()), vr.getErrorClass().toString()); + } + } + } else { + if (binding.getStrength() == BindingStrength.REQUIRED) + txRule(errors, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false, "None of the codes provided are in the value set " + describeReference(binding.getValueSet()) + " (" + valueset.getUrl() + ", and a code from this value set is required) (codes = " + ccSummary(cc) + ")"); + else if (binding.getStrength() == BindingStrength.EXTENSIBLE) { + if (binding.hasExtension("http://hl7.org/fhir/StructureDefinition/elementdefinition-maxValueSet")) + checkMaxValueSet(errors, path, element, profile, ToolingExtensions.readStringExtension(binding, "http://hl7.org/fhir/StructureDefinition/elementdefinition-maxValueSet"), cc, stack); + if (!noExtensibleWarnings) + txWarning(errors, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false,messages.getString("None_of_the_codes_provided_are_in_the_value_set___and_a_code_should_come_from_this_value_set_unless_it_has_no_suitable_code_codes__"), describeReference(binding.getValueSet()), valueset.getUrl(), ccSummary(cc)); + } else if (binding.getStrength() == BindingStrength.PREFERRED) { + if (baseOnly) { + txHint(errors, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false,messages.getString("None_of_the_codes_provided_are_in_the_value_set___and_a_code_is_recommended_to_come_from_this_value_set_codes__"), describeReference(binding.getValueSet()), valueset.getUrl(), ccSummary(cc)); + } + } + } + } else if (vr.getMessage() != null) { + res = false; + txWarning(errors, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false, vr.getMessage()); + } else { + res = false; + } + } + // Then, for any codes that are in code systems we are able + // to validate, we'll validate that the codes actually exist + if (bindingsOk) { + for (Coding nextCoding : cc.getCoding()) { + String nextCode = nextCoding.getCode(); + String nextSystem = nextCoding.getSystem(); + if (isNotBlank(nextCode) && isNotBlank(nextSystem) && context.supportsSystem(nextSystem)) { + ValidationResult vr = context.validateCode(new ValidationOptions(stack.workingLang), nextSystem, nextCode, null); + if (!vr.isOk()) { + txWarning(errors, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false,messages.getString("Code_0_is_not_a_valid_code_in_code_system_1"), nextCode, nextSystem); + } + } + } + } + txTime = txTime + (System.nanoTime() - t); + } + } + } catch (Exception e) { + warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false,messages.getString("Error__validating_CodeableConcept"), e.getMessage()); + } + // special case: if the logical model has both CodeableConcept and Coding mappings, we'll also check the first coding. + if (getMapping("http://hl7.org/fhir/terminology-pattern", logical, logical.getSnapshot().getElementFirstRep()).contains("Coding")) { + checkTerminologyCoding(errors, path, element, profile, theElementCntext, true, true, stack, logical); + } + } + } else if (binding.hasValueSet()) { + hint(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false,messages.getString("Binding_by_URI_reference_cannot_be_checked")); + } else if (!noBindingMsgSuppressed) { + hint(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false,messages.getString("Binding_for_path__has_no_source_so_cant_be_checked"), path); + } + } + } + return res; + } + + private void checkTerminologyCoding(List errors, String path, Element element, StructureDefinition profile, ElementDefinition theElementCntext, boolean inCodeableConcept, boolean checkDisplay, NodeStack stack, StructureDefinition logical) { + Coding c = convertToCoding(element, logical); + String code = c.getCode(); + String system = c.getSystem(); + String display = c.getDisplay(); + rule(errors, IssueType.CODEINVALID, element.line(), element.col(), path, isAbsolute(system),messages.getString("Codingsystem_must_be_an_absolute_reference_not_a_local_reference")); + + if (system != null && code != null && !noTerminologyChecks) { + rule(errors, IssueType.CODEINVALID, element.line(), element.col(), path, !isValueSet(system),messages.getString("The_Coding_references_a_value_set_not_a_code_system_"), system); + try { + if (checkCode(errors, element, path, code, system, display, checkDisplay, stack)) + if (theElementCntext != null && theElementCntext.hasBinding()) { + ElementDefinitionBindingComponent binding = theElementCntext.getBinding(); + if (warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, binding != null,messages.getString("Binding_for__missing"), path)) { + if (binding.hasValueSet()) { + ValueSet valueset = resolveBindingReference(profile, binding.getValueSet(), profile.getUrl()); + if (warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, valueset != null,messages.getString("ValueSet__not_found_by_validator"), describeReference(binding.getValueSet()))) { + try { + long t = System.nanoTime(); + ValidationResult vr = null; + if (binding.getStrength() != BindingStrength.EXAMPLE) { + vr = context.validateCode(new ValidationOptions(stack.workingLang), c, valueset); + } + txTime = txTime + (System.nanoTime() - t); + if (vr != null && !vr.isOk()) { + if (vr.IsNoService()) + txHint(errors, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false,messages.getString("The_value_provided_could_not_be_validated_in_the_absence_of_a_terminology_server")); + else if (vr.getErrorClass() != null && !vr.getErrorClass().isInfrastructure()) { + if (binding.getStrength() == BindingStrength.REQUIRED) + txWarning(errors, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false,messages.getString("Could_not_confirm_that_the_codes_provided_are_in_the_value_set__and_a_code_from_this_value_set_is_required"), describeReference(binding.getValueSet(), valueset)); + else if (binding.getStrength() == BindingStrength.EXTENSIBLE) { + if (binding.hasExtension("http://hl7.org/fhir/StructureDefinition/elementdefinition-maxValueSet")) + checkMaxValueSet(errors, path, element, profile, ToolingExtensions.readStringExtension(binding, "http://hl7.org/fhir/StructureDefinition/elementdefinition-maxValueSet"), c, stack); + else if (!noExtensibleWarnings) + txWarning(errors, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false,messages.getString("Could_not_confirm_that_the_codes_provided_are_in_the_value_set__and_a_code_should_come_from_this_value_set_unless_it_has_no_suitable_code"), describeReference(binding.getValueSet(), valueset)); + } else if (binding.getStrength() == BindingStrength.PREFERRED) { + if (baseOnly) { + txHint(errors, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false,messages.getString("Could_not_confirm_that_the_codes_provided_are_in_the_value_set__and_a_code_is_recommended_to_come_from_this_value_set"), describeReference(binding.getValueSet(), valueset)); + } + } + } else if (binding.getStrength() == BindingStrength.REQUIRED) + txRule(errors, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false, "The Coding provided is not in the value set " + describeReference(binding.getValueSet(), valueset) + ", and a code is required from this value set" + (vr.getMessage() != null ? " (error message = " + vr.getMessage() + ")" : "")); + else if (binding.getStrength() == BindingStrength.EXTENSIBLE) { + if (binding.hasExtension("http://hl7.org/fhir/StructureDefinition/elementdefinition-maxValueSet")) + checkMaxValueSet(errors, path, element, profile, ToolingExtensions.readStringExtension(binding, "http://hl7.org/fhir/StructureDefinition/elementdefinition-maxValueSet"), c, stack); + else + txWarning(errors, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false,messages.getString("The_Coding_provided_is_not_in_the_value_set__and_a_code_should_come_from_this_value_set_unless_it_has_no_suitable_code"), describeReference(binding.getValueSet(), valueset), (vr.getMessage() != null ? " (error message = " + vr.getMessage() + ")" : "")); + } else if (binding.getStrength() == BindingStrength.PREFERRED) { + if (baseOnly) { + txHint(errors, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false,messages.getString("The_Coding_provided_is_not_in_the_value_set__and_a_code_is_recommended_to_come_from_this_value_set"), describeReference(binding.getValueSet(), valueset), (vr.getMessage() != null ? " (error message = " + vr.getMessage() + ")" : "")); + } + } + } + } catch (Exception e) { + warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false,messages.getString("Error__validating_Coding"), e.getMessage()); + } + } + } else if (binding.hasValueSet()) { + hint(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false,messages.getString("Binding_by_URI_reference_cannot_be_checked")); + } else if (!inCodeableConcept && !noBindingMsgSuppressed) { + hint(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false,messages.getString("Binding_for_path__has_no_source_so_cant_be_checked"), path); + } + } + } + } catch (Exception e) { + rule(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false,messages.getString("Error__validating_Coding_"), e.getMessage(), e.toString()); + } + } + } + + private CodeableConcept convertToCodeableConcept(Element element, StructureDefinition logical) { + CodeableConcept res = new CodeableConcept(); + for (ElementDefinition ed : logical.getSnapshot().getElement()) { + if (Utilities.charCount(ed.getPath(), '.') == 1) { + List maps = getMapping("http://hl7.org/fhir/terminology-pattern", logical, ed); + for (String m : maps) { + String name = tail(ed.getPath()); + List list = new ArrayList<>(); + element.getNamedChildren(name, list); + if (!list.isEmpty()) { + if ("Coding.code".equals(m)) { + res.getCodingFirstRep().setCode(list.get(0).primitiveValue()); + } else if ("Coding.system[fmt:OID]".equals(m)) { + String oid = list.get(0).primitiveValue(); + String url = context.oid2Uri(oid); + if (url != null) { + res.getCodingFirstRep().setSystem(url); + } else { + res.getCodingFirstRep().setSystem("urn:oid:" + oid); + } + } else if ("Coding.version".equals(m)) { + res.getCodingFirstRep().setVersion(list.get(0).primitiveValue()); + } else if ("Coding.display".equals(m)) { + res.getCodingFirstRep().setDisplay(list.get(0).primitiveValue()); + } else if ("CodeableConcept.text".equals(m)) { + res.setText(list.get(0).primitiveValue()); + } else if ("CodeableConcept.coding".equals(m)) { + StructureDefinition c = context.fetchTypeDefinition(ed.getTypeFirstRep().getCode()); + for (Element e : list) { + res.addCoding(convertToCoding(e, c)); + } + } + } + } + } + } + return res; + } + + private Coding convertToCoding(Element element, StructureDefinition logical) { + Coding res = new Coding(); + for (ElementDefinition ed : logical.getSnapshot().getElement()) { + if (Utilities.charCount(ed.getPath(), '.') == 1) { + List maps = getMapping("http://hl7.org/fhir/terminology-pattern", logical, ed); + for (String m : maps) { + String name = tail(ed.getPath()); + List list = new ArrayList<>(); + element.getNamedChildren(name, list); + if (!list.isEmpty()) { + if ("Coding.code".equals(m)) { + res.setCode(list.get(0).primitiveValue()); + } else if ("Coding.system[fmt:OID]".equals(m)) { + String oid = list.get(0).primitiveValue(); + String url = context.oid2Uri(oid); + if (url != null) { + res.setSystem(url); + } else { + res.setSystem("urn:oid:" + oid); + } + } else if ("Coding.version".equals(m)) { + res.setVersion(list.get(0).primitiveValue()); + } else if ("Coding.display".equals(m)) { + res.setDisplay(list.get(0).primitiveValue()); + } + } + } + } + } + return res; + } + + private void checkMaxValueSet(List errors, String path, Element element, StructureDefinition profile, String maxVSUrl, CodeableConcept cc, NodeStack stack) { + // TODO Auto-generated method stub + ValueSet valueset = resolveBindingReference(profile, maxVSUrl, profile.getUrl()); + if (warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, valueset != null,messages.getString("ValueSet__not_found_by_validator"), describeReference(maxVSUrl))) { + try { + long t = System.nanoTime(); + ValidationResult vr = context.validateCode(new ValidationOptions(stack.workingLang), cc, valueset); + txTime = txTime + (System.nanoTime() - t); + if (!vr.isOk()) { + if (vr.getErrorClass() != null && vr.getErrorClass().isInfrastructure()) + txWarning(errors, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false,messages.getString("None_of_the_codes_provided_could_be_validated_against_the_maximum_value_set___error__"), describeReference(maxVSUrl), valueset.getUrl(), vr.getMessage()); + else + txRule(errors, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false, "None of the codes provided are in the maximum value set " + describeReference(maxVSUrl) + " (" + valueset.getUrl() + ", and a code from this value set is required) (codes = " + ccSummary(cc) + ")"); + } + } catch (Exception e) { + warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false,messages.getString("Error__validating_CodeableConcept_using_maxValueSet"), e.getMessage()); + } + } + } + + private void checkMaxValueSet(List errors, String path, Element element, StructureDefinition profile, String maxVSUrl, Coding c, NodeStack stack) { + // TODO Auto-generated method stub + ValueSet valueset = resolveBindingReference(profile, maxVSUrl, profile.getUrl()); + if (warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, valueset != null,messages.getString("ValueSet__not_found_by_validator"), describeReference(maxVSUrl))) { + try { + long t = System.nanoTime(); + ValidationResult vr = context.validateCode(new ValidationOptions(stack.workingLang), c, valueset); + txTime = txTime + (System.nanoTime() - t); + if (!vr.isOk()) { + if (vr.getErrorClass() != null && vr.getErrorClass().isInfrastructure()) + txWarning(errors, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false,messages.getString("The_code_provided_could_not_be_validated_against_the_maximum_value_set___error__"), describeReference(maxVSUrl), valueset.getUrl(), vr.getMessage()); + else + txRule(errors, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false, "The code provided is not in the maximum value set " + describeReference(maxVSUrl) + " (" + valueset.getUrl() + ", and a code from this value set is required) (code = " + c.getSystem() + "#" + c.getCode() + ")"); + } + } catch (Exception e) { + warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false,messages.getString("Error__validating_CodeableConcept_using_maxValueSet"), e.getMessage()); + } + } + } + + private void checkMaxValueSet(List errors, String path, Element element, StructureDefinition profile, String maxVSUrl, String value, NodeStack stack) { + // TODO Auto-generated method stub + ValueSet valueset = resolveBindingReference(profile, maxVSUrl, profile.getUrl()); + if (warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, valueset != null,messages.getString("ValueSet__not_found_by_validator"), describeReference(maxVSUrl))) { + try { + long t = System.nanoTime(); + ValidationResult vr = context.validateCode(new ValidationOptions(stack.workingLang), value, valueset); + txTime = txTime + (System.nanoTime() - t); + if (!vr.isOk()) { + if (vr.getErrorClass() != null && vr.getErrorClass().isInfrastructure()) + txWarning(errors, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false,messages.getString("The_code_provided_could_not_be_validated_against_the_maximum_value_set___error__"), describeReference(maxVSUrl), valueset.getUrl(), vr.getMessage()); + else + txRule(errors, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false, "The code provided is not in the maximum value set " + describeReference(maxVSUrl) + " (" + valueset.getUrl() + "), and a code from this value set is required) (code = " + value + "), (error = " + vr.getMessage() + ")"); + } + } catch (Exception e) { + warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false,messages.getString("Error__validating_CodeableConcept_using_maxValueSet"), e.getMessage()); + } + } + } + + private String ccSummary(CodeableConcept cc) { + CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder(); + for (Coding c : cc.getCoding()) + b.append(c.getSystem() + "#" + c.getCode()); + return b.toString(); + } + + private void checkCoding(List errors, String path, Element focus, Coding fixed, String fixedSource, boolean pattern) { + checkFixedValue(errors, path + ".system", focus.getNamedChild("system"), fixed.getSystemElement(), fixedSource, "system", focus, pattern); + checkFixedValue(errors, path + ".version", focus.getNamedChild("version"), fixed.getVersionElement(), fixedSource, "version", focus, pattern); + checkFixedValue(errors, path + ".code", focus.getNamedChild("code"), fixed.getCodeElement(), fixedSource, "code", focus, pattern); + checkFixedValue(errors, path + ".display", focus.getNamedChild("display"), fixed.getDisplayElement(), fixedSource, "display", focus, pattern); + checkFixedValue(errors, path + ".userSelected", focus.getNamedChild("userSelected"), fixed.getUserSelectedElement(), fixedSource, "userSelected", focus, pattern); + } + + private void checkCoding(List errors, String path, Element element, StructureDefinition profile, ElementDefinition theElementCntext, boolean inCodeableConcept, boolean checkDisplay, NodeStack stack) { + String code = element.getNamedChildValue("code"); + String system = element.getNamedChildValue("system"); + String display = element.getNamedChildValue("display"); + rule(errors, IssueType.CODEINVALID, element.line(), element.col(), path, isAbsolute(system),messages.getString("Codingsystem_must_be_an_absolute_reference_not_a_local_reference")); + + if (system != null && code != null && !noTerminologyChecks) { + rule(errors, IssueType.CODEINVALID, element.line(), element.col(), path, !isValueSet(system),messages.getString("The_Coding_references_a_value_set_not_a_code_system_"), system); + try { + if (checkCode(errors, element, path, code, system, display, checkDisplay, stack)) + if (theElementCntext != null && theElementCntext.hasBinding()) { + ElementDefinitionBindingComponent binding = theElementCntext.getBinding(); + if (warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, binding != null,messages.getString("Binding_for__missing"), path)) { + if (binding.hasValueSet()) { + ValueSet valueset = resolveBindingReference(profile, binding.getValueSet(), profile.getUrl()); + if (warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, valueset != null,messages.getString("ValueSet__not_found_by_validator"), describeReference(binding.getValueSet()))) { + try { + Coding c = ObjectConverter.readAsCoding(element); + long t = System.nanoTime(); + ValidationResult vr = null; + if (binding.getStrength() != BindingStrength.EXAMPLE) { + vr = context.validateCode(new ValidationOptions(stack.workingLang), c, valueset); + } + txTime = txTime + (System.nanoTime() - t); + if (vr != null && !vr.isOk()) { + if (vr.IsNoService()) + txHint(errors, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false,messages.getString("The_value_provided_could_not_be_validated_in_the_absence_of_a_terminology_server")); + else if (vr.getErrorClass() != null && !vr.getErrorClass().isInfrastructure()) { + if (binding.getStrength() == BindingStrength.REQUIRED) + txWarning(errors, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false,messages.getString("Could_not_confirm_that_the_codes_provided_are_in_the_value_set__and_a_code_from_this_value_set_is_required"), describeReference(binding.getValueSet(), valueset)); + else if (binding.getStrength() == BindingStrength.EXTENSIBLE) { + if (binding.hasExtension("http://hl7.org/fhir/StructureDefinition/elementdefinition-maxValueSet")) + checkMaxValueSet(errors, path, element, profile, ToolingExtensions.readStringExtension(binding, "http://hl7.org/fhir/StructureDefinition/elementdefinition-maxValueSet"), c, stack); + else if (!noExtensibleWarnings) + txWarning(errors, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false,messages.getString("Could_not_confirm_that_the_codes_provided_are_in_the_value_set__and_a_code_should_come_from_this_value_set_unless_it_has_no_suitable_code"), describeReference(binding.getValueSet(), valueset)); + } else if (binding.getStrength() == BindingStrength.PREFERRED) { + if (baseOnly) { + txHint(errors, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false,messages.getString("Could_not_confirm_that_the_codes_provided_are_in_the_value_set__and_a_code_is_recommended_to_come_from_this_value_set"), describeReference(binding.getValueSet(), valueset)); + } + } + } else if (binding.getStrength() == BindingStrength.REQUIRED) + txRule(errors, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false, "The Coding provided is not in the value set " + describeReference(binding.getValueSet(), valueset) + ", and a code is required from this value set. " + getErrorMessage(vr.getMessage())); + else if (binding.getStrength() == BindingStrength.EXTENSIBLE) { + if (binding.hasExtension("http://hl7.org/fhir/StructureDefinition/elementdefinition-maxValueSet")) + checkMaxValueSet(errors, path, element, profile, ToolingExtensions.readStringExtension(binding, "http://hl7.org/fhir/StructureDefinition/elementdefinition-maxValueSet"), c, stack); + else + txWarning(errors, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false,messages.getString("The_Coding_provided_is_not_in_the_value_set__and_a_code_should_come_from_this_value_set_unless_it_has_no_suitable_code_"), describeReference(binding.getValueSet(), valueset), getErrorMessage(vr.getMessage())); + } else if (binding.getStrength() == BindingStrength.PREFERRED) { + if (baseOnly) { + txHint(errors, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false,messages.getString("The_Coding_provided_is_not_in_the_value_set__and_a_code_is_recommended_to_come_from_this_value_set_"), describeReference(binding.getValueSet(), valueset), getErrorMessage(vr.getMessage())); + } + } + } + } catch (Exception e) { + warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false,messages.getString("Error__validating_Coding"), e.getMessage()); + } + } + } else if (binding.hasValueSet()) { + hint(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false,messages.getString("Binding_by_URI_reference_cannot_be_checked")); + } else if (!inCodeableConcept && !noBindingMsgSuppressed) { + hint(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false,messages.getString("Binding_for_path__has_no_source_so_cant_be_checked"), path); + } + } + } + } catch (Exception e) { + rule(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false,messages.getString("Error__validating_Coding_"), e.getMessage(), e.toString()); + } + } + } + + private boolean isValueSet(String url) { + try { + ValueSet vs = context.fetchResourceWithException(ValueSet.class, url); + return vs != null; + } catch (Exception e) { + return false; + } + } + + private void checkContactPoint(List errors, String path, Element focus, ContactPoint fixed, String fixedSource, boolean pattern) { + checkFixedValue(errors, path + ".system", focus.getNamedChild("system"), fixed.getSystemElement(), fixedSource, "system", focus, pattern); + checkFixedValue(errors, path + ".value", focus.getNamedChild("value"), fixed.getValueElement(), fixedSource, "value", focus, pattern); + checkFixedValue(errors, path + ".use", focus.getNamedChild("use"), fixed.getUseElement(), fixedSource, "use", focus, pattern); + checkFixedValue(errors, path + ".period", focus.getNamedChild("period"), fixed.getPeriod(), fixedSource, "period", focus, pattern); + + } + + private StructureDefinition checkExtension(ValidatorHostContext hostContext, List errors, String path, Element resource, Element container, Element element, ElementDefinition def, StructureDefinition profile, NodeStack stack, NodeStack containerStack, String extensionUrl) throws FHIRException { + String url = element.getNamedChildValue("url"); + boolean isModifier = element.getName().equals("modifierExtension"); + + long t = System.nanoTime(); + StructureDefinition ex = Utilities.isAbsoluteUrl(url) ? context.fetchResource(StructureDefinition.class, url) : null; + sdTime = sdTime + (System.nanoTime() - t); + if (ex == null) { + if (xverManager == null) { + xverManager = new XVerExtensionManager(context); + } + if (xverManager.matchingUrl(url)) { + switch (xverManager.status(url)) { + case BadVersion: + rule(errors, IssueType.INVALID, element.line(), element.col(), path + "[url='" + url + "']", false,messages.getString("Extension_url__is_not_valid_invalidVersion_"), url, xverManager.getVersion(url)); + break; + case Unknown: + rule(errors, IssueType.INVALID, element.line(), element.col(), path + "[url='" + url + "']", false,messages.getString("Extension_url__is_not_valid_unknown_Element_id_"), url, xverManager.getElementId(url)); + break; + case Invalid: + rule(errors, IssueType.INVALID, element.line(), element.col(), path + "[url='" + url + "']", false,messages.getString("Extension_url__is_not_valid_Element_id__is_valid_but_cannot_be_used_in_a_crossversion_paradigm_because_there_has_been_no_changes_across_the_relevant_versions"), url, xverManager.getElementId(url)); + break; + case Valid: + ex = xverManager.makeDefinition(url); + context.generateSnapshot(ex); + context.cacheResource(ex); + break; + default: + rule(errors, IssueType.INVALID, element.line(), element.col(), path + "[url='" + url + "']", false,messages.getString("Extension_url__evaluation_state_illegal"), url); + break; + } + } else if (extensionUrl != null && !isAbsolute(url)) { + if (extensionUrl.equals(profile.getUrl())) { + rule(errors, IssueType.INVALID, element.line(), element.col(), path + "[url='" + url + "']", hasExtensionSlice(profile, url),messages.getString("Subextension_url__is_not_defined_by_the_Extension_"), url, profile.getUrl()); + } + } else if (rule(errors, IssueType.STRUCTURE, element.line(), element.col(), path, allowUnknownExtension(url),messages.getString("The_extension__is_unknown_and_not_allowed_here"), url)) { + hint(errors, IssueType.STRUCTURE, element.line(), element.col(), path, isKnownExtension(url),messages.getString("Unknown_extension_"), url); + } + } + if (ex != null) { + trackUsage(ex, hostContext, element); + if (def.getIsModifier()) { + rule(errors, IssueType.STRUCTURE, element.line(), element.col(), path + "[url='" + url + "']", ex.getSnapshot().getElement().get(0).getIsModifier(),messages.getString("Extension_modifier_mismatch_the_extension_element_is_labelled_as_a_modifier_but_the_underlying_extension_is_not")); + } else { + rule(errors, IssueType.STRUCTURE, element.line(), element.col(), path + "[url='" + url + "']", !ex.getSnapshot().getElement().get(0).getIsModifier(),messages.getString("Extension_modifier_mismatch_the_extension_element_is_not_labelled_as_a_modifier_but_the_underlying_extension_is")); + } + // two questions + // 1. can this extension be used here? + checkExtensionContext(errors, resource, container, ex, containerStack, hostContext); + + if (isModifier) + rule(errors, IssueType.STRUCTURE, element.line(), element.col(), path + "[url='" + url + "']", ex.getSnapshot().getElement().get(0).getIsModifier(),messages.getString("The_Extension__must_be_used_as_a_modifierExtension"), url); + else + rule(errors, IssueType.STRUCTURE, element.line(), element.col(), path + "[url='" + url + "']", !ex.getSnapshot().getElement().get(0).getIsModifier(),messages.getString("The_Extension__must_not_be_used_as_an_extension_its_a_modifierExtension"), url); + + // check the type of the extension: + Set allowedTypes = listExtensionTypes(ex); + String actualType = getExtensionType(element); + if (actualType == null) + rule(errors, IssueType.STRUCTURE, element.line(), element.col(), path, allowedTypes.isEmpty(),messages.getString("The_Extension__definition_is_for_a_simple_extension_so_it_must_contain_a_value_not_extensions"), url); + else + rule(errors, IssueType.STRUCTURE, element.line(), element.col(), path, allowedTypes.contains(actualType),messages.getString("The_Extension__definition_allows_for_the_types__but_found_type_"), url, allowedTypes.toString(), actualType); + + // 3. is the content of the extension valid? + validateElement(hostContext, errors, ex, ex.getSnapshot().getElement().get(0), null, null, resource, element, "Extension", stack, false, true, url); + + } + return ex; + } + + private boolean hasExtensionSlice(StructureDefinition profile, String sliceName) { + for (ElementDefinition ed : profile.getSnapshot().getElement()) { + if (ed.getPath().equals("Extension.extension.url") && ed.hasFixed() && sliceName.equals(ed.getFixed().primitiveValue())) { + return true; + } + } + return false; + } + + private String getExtensionType(Element element) { + for (Element e : element.getChildren()) { + if (e.getName().startsWith("value")) { + String tn = e.getName().substring(5); + String ltn = Utilities.uncapitalize(tn); + if (isPrimitiveType(ltn)) + return ltn; else - ft = tryParse(ref); + return tn; + } + } + return null; + } - if (reference.hasType()) { // R4 onwards... - // the type has to match the specified - String tu = isAbsolute(reference.getType()) ? reference.getType() : "http://hl7.org/fhir/StructureDefinition/" + reference.getType(); - TypeRefComponent containerType = container.getType("Reference"); - if (!containerType.hasTargetProfile(tu) && !containerType.hasTargetProfile("http://hl7.org/fhir/StructureDefinition/Resource")) { - boolean matchingResource = false; - for (CanonicalType target : containerType.getTargetProfile()) { - StructureDefinition sd = resolveProfile(profile, target.asStringValue()); - if (("http://hl7.org/fhir/StructureDefinition/" + sd.getType()).equals(tu)) { - matchingResource = true; - break; - } - } - rule(errors, IssueType.STRUCTURE, element.line(), element.col(), path, matchingResource, - "The type '" + reference.getType() + "' is not a valid Target for this element (must be one of " + container.getType("Reference").getTargetProfile() + ")"); + private Set listExtensionTypes(StructureDefinition ex) { + ElementDefinition vd = null; + for (ElementDefinition ed : ex.getSnapshot().getElement()) { + if (ed.getPath().startsWith("Extension.value")) { + vd = ed; + break; + } + } + Set res = new HashSet(); + if (vd != null && !"0".equals(vd.getMax())) { + for (TypeRefComponent tr : vd.getType()) { + res.add(tr.getWorkingCode()); + } + } + return res; + } - } - // the type has to match the actual - rule(errors, IssueType.STRUCTURE, element.line(), element.col(), path, ft == null || ft.equals(reference.getType()), "The specified type '" + reference.getType() + "' does not match the found type '" + ft + "'"); - } + private boolean checkExtensionContext(List errors, Element resource, Element container, StructureDefinition definition, NodeStack stack, ValidatorHostContext hostContext) { + String extUrl = definition.getUrl(); + boolean ok = false; + CommaSeparatedStringBuilder contexts = new CommaSeparatedStringBuilder(); + List plist = new ArrayList<>(); + plist.add(stripIndexes(stack.getLiteralPath())); + for (String s : stack.getLogicalPaths()) { + String p = stripIndexes(s); + // all extensions are always allowed in ElementDefinition.example.value, and in fixed and pattern values. TODO: determine the logical paths from the path stated in the element definition.... + if (Utilities.existsInList(p, "ElementDefinition.example.value", "ElementDefinition.pattern", "ElementDefinition.fixed")) { + return true; + } + plist.add(p); - if (we != null && pol.checkType()) { - if (warning(errors, IssueType.STRUCTURE, element.line(), element.col(), path, ft != null, "Unable to determine type of target resource")) { - // we validate as much as we can. First, can we infer a type from the profile? - boolean ok = false; - TypeRefComponent type = getReferenceTypeRef(container.getType()); - if (type.hasTargetProfile() && !type.hasTargetProfile("http://hl7.org/fhir/StructureDefinition/Resource")) { - Set types = new HashSet<>(); - List profiles = new ArrayList<>(); - for (UriType u : type.getTargetProfile()) { - StructureDefinition sd = resolveProfile(profile, u.getValue()); - if (rule(errors, IssueType.STRUCTURE, element.line(), element.col(), path, sd != null, "Unable to resolve the profile reference '" + u.getValue() + "'")) { - types.add(sd.getType()); - if (ft.equals(sd.getType())) { - ok = true; - profiles.add(sd); - } - } - } - if (!pol.checkValid()) { - rule(errors, IssueType.STRUCTURE, element.line(), element.col(), path, profiles.size() > 0, "Unable to find matching profile for " + ref + " (by type) among choices: " + StringUtils.join("; ", type.getTargetProfile())); - } else { - Map> badProfiles = new HashMap>(); - Map> goodProfiles = new HashMap>(); - int goodCount = 0; - for (StructureDefinition pr : profiles) { - List profileErrors = new ArrayList(); - validateResource(we.hostContext(hostContext, pr), profileErrors, we.getResource(), we.getFocus(), pr, IdStatus.OPTIONAL, we.getStack()); - if (!hasErrors(profileErrors)) { - goodCount++; - goodProfiles.put(pr, profileErrors); - trackUsage(pr, hostContext, element); - } else { - badProfiles.put(pr, profileErrors); - } - } - if (goodCount == 1) { - if (showMessagesFromReferences) { - for (ValidationMessage vm : goodProfiles.values().iterator().next()) { - if (!errors.contains(vm)) { - errors.add(vm); - } - } - } - - } else if (goodProfiles.size() == 0) { - if (!isShowMessagesFromReferences()) { - rule(errors, IssueType.STRUCTURE, element.line(), element.col(), path, areAllBaseProfiles(profiles), "Unable to find matching profile for " + ref + " among choices: " + asList(type.getTargetProfile())); - for (StructureDefinition sd : badProfiles.keySet()) { - slicingHint(errors, IssueType.STRUCTURE, element.line(), element.col(), path, false, "Details for " + ref + " matching against Profile" + sd.getUrl(), errorSummaryForSlicingAsHtml(badProfiles.get(sd))); - } - } else { - rule(errors, IssueType.STRUCTURE, element.line(), element.col(), path, profiles.size() == 1, "Unable to find matching profile for " + ref + " among choices: " + asList(type.getTargetProfile())); - for (List messages : badProfiles.values()) { - for (ValidationMessage vm : messages) { - if (!errors.contains(vm)) { - errors.add(vm); - } - } - } - } - } else { - if (!isShowMessagesFromReferences()) { - warning(errors, IssueType.STRUCTURE, element.line(), element.col(), path, false, "Found multiple matching profiles for " + ref + " among choices: " + asListByUrl(goodProfiles.keySet())); - for (StructureDefinition sd : badProfiles.keySet()) { - slicingHint(errors, IssueType.STRUCTURE, element.line(), element.col(), path, false, "Details for " + ref + " matching against Profile" + sd.getUrl(), errorSummaryForSlicingAsHtml(badProfiles.get(sd))); - } - } else { - warning(errors, IssueType.STRUCTURE, element.line(), element.col(), path, false, "Found multiple matching profiles for " + ref + " among choices: " + asListByUrl(goodProfiles.keySet())); - for (List messages : goodProfiles.values()) { - for (ValidationMessage vm : messages) { - if (!errors.contains(vm)) { - errors.add(vm); - } - } - } - } - } - } - rule(errors, IssueType.STRUCTURE, element.line(), element.col(), path, ok, "Invalid Resource target type. Found " + ft + ", but expected one of (" + types.toString() + ")"); - } - if (type.hasAggregation()) { - boolean modeOk = false; - for (Enumeration mode : type.getAggregation()) { - if (mode.getValue().equals(AggregationMode.CONTAINED) && refType.equals("contained")) - modeOk = true; - else if (mode.getValue().equals(AggregationMode.BUNDLED) && refType.equals("bundled")) - modeOk = true; - else if (mode.getValue().equals(AggregationMode.REFERENCED) && (refType.equals("bundled") || refType.equals("remote"))) - modeOk = true; - } - rule(errors, IssueType.STRUCTURE, element.line(), element.col(), path, modeOk, "Reference is " + refType + " which isn't supported by the specified aggregation mode(s) for the reference"); - } - } - } - if (we == null) { - TypeRefComponent type = getReferenceTypeRef(container.getType()); - boolean okToRef = !type.hasAggregation() || type.hasAggregation(AggregationMode.REFERENCED); - rule(errors, IssueType.REQUIRED, -1, -1, path, okToRef, "Bundled or contained reference not found within the bundle/resource " + ref); - } - if (we == null && ft != null && assumeValidRestReferences) { - // if we == null, we inferred ft from the reference. if we are told to treat this as gospel - TypeRefComponent type = getReferenceTypeRef(container.getType()); - Set types = new HashSet<>(); - for (CanonicalType tp : type.getTargetProfile()) { - StructureDefinition sd = context.fetchResource(StructureDefinition.class, tp.getValue()); - if (sd != null) { - types.add(sd.getType()); - } - } - rule(errors, IssueType.STRUCTURE, element.line(), element.col(), path, types.isEmpty() || types.contains(ft), "The type '" + ft + "' implied by the reference URL " + ref + " is not a valid Target for this element (must be one of " + types + ")"); - - } - if (pol == ReferenceValidationPolicy.CHECK_VALID) { - // todo.... - } } - private String asListByUrl(Collection list) { - CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder(); - for (StructureDefinition sd : list) { - b.append(sd.getUrl()); + for (StructureDefinitionContextComponent ctxt : fixContexts(extUrl, definition.getContext())) { + if (ok) { + break; + } + if (ctxt.getType() == ExtensionContextType.ELEMENT) { + String en = ctxt.getExpression(); + contexts.append("e:" + en); + if ("Element".equals(en)) { + ok = true; + } else if (en.equals("Resource") && container.isResource()) { + ok = true; } - return b.toString(); - } - - private String asList(Collection list) { - CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder(); - for (CanonicalType c : list) { - b.append(c.getValue()); - } - return b.toString(); - } - - private boolean areAllBaseProfiles(List profiles) { - for (StructureDefinition sd : profiles) { - if (!sd.getUrl().startsWith("http://hl7.org/fhir/StructureDefinition/")) { - return false; + for (String p : plist) { + if (ok) { + break; + } + if (p.equals(en)) { + ok = true; + } else { + String pn = p; + String pt = ""; + if (p.contains(".")) { + pn = p.substring(0, p.indexOf(".")); + pt = p.substring(p.indexOf(".")); } + StructureDefinition sd = context.fetchTypeDefinition(pn); + while (sd != null) { + if ((sd.getType() + pt).equals(en)) { + ok = true; + break; + } + if (sd.getBaseDefinition() != null) { + sd = context.fetchResource(StructureDefinition.class, sd.getBaseDefinition()); + } else { + sd = null; + } + } + } } + } else if (ctxt.getType() == ExtensionContextType.EXTENSION) { + contexts.append("x:" + ctxt.getExpression()); + NodeStack estack = stack.parent; + if (estack != null && estack.getElement().fhirType().equals("Extension")) { + String ext = estack.element.getNamedChildValue("url"); + if (ctxt.getExpression().equals(ext)) { + ok = true; + } + } + } else if (ctxt.getType() == ExtensionContextType.FHIRPATH) { + contexts.append("p:" + ctxt.getExpression()); + // The context is all elements that match the FHIRPath query found in the expression. + List res = fpe.evaluate(hostContext, resource, hostContext.getRootResource(), container, fpe.parse(ctxt.getExpression())); + if (res.contains(container)) { + ok = true; + } + } else { + throw new Error("Unrecognised extension context " + ctxt.getTypeElement().asStringValue()); + } + } + if (!ok) { + rule(errors, IssueType.STRUCTURE, container.line(), container.col(), stack.literalPath, false,messages.getString("The_extension__is_not_allowed_to_be_used_at_this_point_allowed___this_element_is_"), extUrl, contexts.toString(), plist.toString()); + return false; + } else { + if (definition.hasContextInvariant()) { + for (StringType s : definition.getContextInvariant()) { + if (!fpe.evaluateToBoolean(hostContext, resource, hostContext.getRootResource(), container, fpe.parse(s.getValue()))) { + rule(errors, IssueType.STRUCTURE, container.line(), container.col(), stack.literalPath, false,messages.getString("The_extension__is_not_allowed_to_be_used_at_this_point_based_on_context_invariant_"), extUrl, s.getValue()); + return false; + } + } + } + return true; + } + } + + private List fixContexts(String extUrl, List list) { + List res = new ArrayList<>(); + for (StructureDefinitionContextComponent ctxt : list) { + res.add(ctxt.copy()); + } + if ("http://hl7.org/fhir/StructureDefinition/structuredefinition-fhir-type".equals(extUrl)) { + list.get(0).setExpression("ElementDefinition.type"); + } + if ("http://hl7.org/fhir/StructureDefinition/regex".equals(extUrl)) { + list.get(1).setExpression("ElementDefinition.type"); + } + return list; + } + + private String stripIndexes(String path) { + boolean skip = false; + StringBuilder b = new StringBuilder(); + for (char c : path.toCharArray()) { + if (skip) { + if (c == ']') { + skip = false; + } + } else if (c == '[') { + skip = true; + } else { + b.append(c); + } + } + return b.toString(); + } + + private void checkFixedValue(List errors, String path, Element focus, org.hl7.fhir.r5.model.Element fixed, String fixedSource, String propName, Element parent) { + checkFixedValue(errors, path, focus, fixed, fixedSource, propName, parent, false); + } + + @SuppressWarnings("rawtypes") + private void checkFixedValue(List errors, String path, Element focus, org.hl7.fhir.r5.model.Element fixed, String fixedSource, String propName, Element parent, boolean pattern) { + if ((fixed == null || fixed.isEmpty()) && focus == null) { + ; // this is all good + } else if ((fixed == null || fixed.isEmpty()) && focus != null) { + rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, pattern,messages.getString("The_element__is_present_in_the_instance_but_not_allowed_in_the_applicable__specified_in_profile"), focus.getName(), (pattern ? "pattern" : "fixed value")); + } else if (fixed != null && !fixed.isEmpty() && focus == null) { + rule(errors, IssueType.VALUE, parent == null ? -1 : parent.line(), parent == null ? -1 : parent.col(), path, false,messages.getString("Missing_element___required_by_fixed_value_assigned_in_profile_"), propName, fixedSource); + } else { + String value = focus.primitiveValue(); + if (fixed instanceof org.hl7.fhir.r5.model.BooleanType) + rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.r5.model.BooleanType) fixed).asStringValue(), value),messages.getString("Value_is__but_must_be_"), value, ((org.hl7.fhir.r5.model.BooleanType) fixed).asStringValue()); + else if (fixed instanceof org.hl7.fhir.r5.model.IntegerType) + rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.r5.model.IntegerType) fixed).asStringValue(), value),messages.getString("Value_is__but_must_be_"), value, ((org.hl7.fhir.r5.model.IntegerType) fixed).asStringValue()); + else if (fixed instanceof org.hl7.fhir.r5.model.DecimalType) + rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.r5.model.DecimalType) fixed).asStringValue(), value),messages.getString("Value_is__but_must_be_"), value, ((org.hl7.fhir.r5.model.DecimalType) fixed).asStringValue()); + else if (fixed instanceof org.hl7.fhir.r5.model.Base64BinaryType) + rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.r5.model.Base64BinaryType) fixed).asStringValue(), value),messages.getString("Value_is__but_must_be_"), value, ((org.hl7.fhir.r5.model.Base64BinaryType) fixed).asStringValue()); + else if (fixed instanceof org.hl7.fhir.r5.model.InstantType) + rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.r5.model.InstantType) fixed).getValue().toString(), value),messages.getString("Value_is__but_must_be_"), value, ((org.hl7.fhir.r5.model.InstantType) fixed).asStringValue()); + else if (fixed instanceof org.hl7.fhir.r5.model.CodeType) + rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.r5.model.CodeType) fixed).getValue(), value),messages.getString("Value_is__but_must_be_"), value, ((org.hl7.fhir.r5.model.CodeType) fixed).getValue()); + else if (fixed instanceof org.hl7.fhir.r5.model.Enumeration) + rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.r5.model.Enumeration) fixed).asStringValue(), value),messages.getString("Value_is__but_must_be_"), value, ((org.hl7.fhir.r5.model.Enumeration) fixed).asStringValue()); + else if (fixed instanceof org.hl7.fhir.r5.model.StringType) + rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.r5.model.StringType) fixed).getValue(), value),messages.getString("Value_is__but_must_be_"), value, ((org.hl7.fhir.r5.model.StringType) fixed).getValue()); + else if (fixed instanceof org.hl7.fhir.r5.model.UriType) + rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.r5.model.UriType) fixed).getValue(), value),messages.getString("Value_is__but_must_be_"), value, ((org.hl7.fhir.r5.model.UriType) fixed).getValue()); + else if (fixed instanceof org.hl7.fhir.r5.model.DateType) + rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.r5.model.DateType) fixed).getValue().toString(), value),messages.getString("Value_is__but_must_be_"), value, ((org.hl7.fhir.r5.model.DateType) fixed).getValue()); + else if (fixed instanceof org.hl7.fhir.r5.model.DateTimeType) + rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.r5.model.DateTimeType) fixed).getValue().toString(), value),messages.getString("Value_is__but_must_be_"), value, ((org.hl7.fhir.r5.model.DateTimeType) fixed).getValue()); + else if (fixed instanceof org.hl7.fhir.r5.model.OidType) + rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.r5.model.OidType) fixed).getValue(), value),messages.getString("Value_is__but_must_be_"), value, ((org.hl7.fhir.r5.model.OidType) fixed).getValue()); + else if (fixed instanceof org.hl7.fhir.r5.model.UuidType) + rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.r5.model.UuidType) fixed).getValue(), value),messages.getString("Value_is__but_must_be_"), value, ((org.hl7.fhir.r5.model.UuidType) fixed).getValue()); + else if (fixed instanceof org.hl7.fhir.r5.model.IdType) + rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.r5.model.IdType) fixed).getValue(), value),messages.getString("Value_is__but_must_be_"), value, ((org.hl7.fhir.r5.model.IdType) fixed).getValue()); + else if (fixed instanceof Quantity) + checkQuantity(errors, path, focus, (Quantity) fixed, fixedSource, pattern); + else if (fixed instanceof Address) + checkAddress(errors, path, focus, (Address) fixed, fixedSource, pattern); + else if (fixed instanceof ContactPoint) + checkContactPoint(errors, path, focus, (ContactPoint) fixed, fixedSource, pattern); + else if (fixed instanceof Attachment) + checkAttachment(errors, path, focus, (Attachment) fixed, fixedSource, pattern); + else if (fixed instanceof Identifier) + checkIdentifier(errors, path, focus, (Identifier) fixed, fixedSource, pattern); + else if (fixed instanceof Coding) + checkCoding(errors, path, focus, (Coding) fixed, fixedSource, pattern); + else if (fixed instanceof HumanName) + checkHumanName(errors, path, focus, (HumanName) fixed, fixedSource, pattern); + else if (fixed instanceof CodeableConcept) + checkCodeableConcept(errors, path, focus, (CodeableConcept) fixed, fixedSource, pattern); + else if (fixed instanceof Timing) + checkTiming(errors, path, focus, (Timing) fixed, fixedSource, pattern); + else if (fixed instanceof Period) + checkPeriod(errors, path, focus, (Period) fixed, fixedSource, pattern); + else if (fixed instanceof Range) + checkRange(errors, path, focus, (Range) fixed, fixedSource, pattern); + else if (fixed instanceof Ratio) + checkRatio(errors, path, focus, (Ratio) fixed, fixedSource, pattern); + else if (fixed instanceof SampledData) + checkSampledData(errors, path, focus, (SampledData) fixed, fixedSource, pattern); + + else + rule(errors, IssueType.EXCEPTION, focus.line(), focus.col(), path, false,messages.getString("Unhandled_fixed_value_type_"), fixed.getClass().getName()); + List extensions = new ArrayList(); + focus.getNamedChildren("extension", extensions); + if (fixed.getExtension().size() == 0) { + rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, extensions.size() == 0,messages.getString("No_extensions_allowed_as_the_specified_fixed_value_doesnt_contain_any_extensions")); + } else if (rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, extensions.size() == fixed.getExtension().size(),messages.getString("Extensions_count_mismatch_expected__but_found_"), Integer.toString(fixed.getExtension().size()), Integer.toString(extensions.size()))) { + for (Extension e : fixed.getExtension()) { + Element ex = getExtensionByUrl(extensions, e.getUrl()); + if (rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, ex != null,messages.getString("Extension_count_mismatch_unable_to_find_extension_"), e.getUrl())) { + checkFixedValue(errors, path, ex.getNamedChild("extension").getNamedChild("value"), e.getValue(), fixedSource, "extension.value", ex.getNamedChild("extension")); + } + } + } + } + } + + private void checkHumanName(List errors, String path, Element focus, HumanName fixed, String fixedSource, boolean pattern) { + checkFixedValue(errors, path + ".use", focus.getNamedChild("use"), fixed.getUseElement(), fixedSource, "use", focus, pattern); + checkFixedValue(errors, path + ".text", focus.getNamedChild("text"), fixed.getTextElement(), fixedSource, "text", focus, pattern); + checkFixedValue(errors, path + ".period", focus.getNamedChild("period"), fixed.getPeriod(), fixedSource, "period", focus, pattern); + + List parts = new ArrayList(); + focus.getNamedChildren("family", parts); + if (rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, parts.size() > 0 == fixed.hasFamily(),messages.getString("Expected__but_found__family_elements"), (fixed.hasFamily() ? "1" : "0"), Integer.toString(parts.size()))) { + for (int i = 0; i < parts.size(); i++) + checkFixedValue(errors, path + ".family", parts.get(i), fixed.getFamilyElement(), fixedSource, "family", focus, pattern); + } + focus.getNamedChildren("given", parts); + if (rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, parts.size() == fixed.getGiven().size(),messages.getString("Expected__but_found__given_elements"), Integer.toString(fixed.getGiven().size()), Integer.toString(parts.size()))) { + for (int i = 0; i < parts.size(); i++) + checkFixedValue(errors, path + ".given", parts.get(i), fixed.getGiven().get(i), fixedSource, "given", focus, pattern); + } + focus.getNamedChildren("prefix", parts); + if (rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, parts.size() == fixed.getPrefix().size(),messages.getString("Expected__but_found__prefix_elements"), Integer.toString(fixed.getPrefix().size()), Integer.toString(parts.size()))) { + for (int i = 0; i < parts.size(); i++) + checkFixedValue(errors, path + ".prefix", parts.get(i), fixed.getPrefix().get(i), fixedSource, "prefix", focus, pattern); + } + focus.getNamedChildren("suffix", parts); + if (rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, parts.size() == fixed.getSuffix().size(),messages.getString("Expected__but_found__suffix_elements"), Integer.toString(fixed.getSuffix().size()), Integer.toString(parts.size()))) { + for (int i = 0; i < parts.size(); i++) + checkFixedValue(errors, path + ".suffix", parts.get(i), fixed.getSuffix().get(i), fixedSource, "suffix", focus, pattern); + } + } + + private void checkIdentifier(List errors, String path, Element element, ElementDefinition context) { + String system = element.getNamedChildValue("system"); + rule(errors, IssueType.CODEINVALID, element.line(), element.col(), path, isAbsolute(system),messages.getString("Identifiersystem_must_be_an_absolute_reference_not_a_local_reference")); + } + + private void checkIdentifier(List errors, String path, Element focus, Identifier fixed, String fixedSource, boolean pattern) { + checkFixedValue(errors, path + ".use", focus.getNamedChild("use"), fixed.getUseElement(), fixedSource, "use", focus, pattern); + checkFixedValue(errors, path + ".type", focus.getNamedChild("type"), fixed.getType(), fixedSource, "type", focus, pattern); + checkFixedValue(errors, path + ".system", focus.getNamedChild("system"), fixed.getSystemElement(), fixedSource, "system", focus, pattern); + checkFixedValue(errors, path + ".value", focus.getNamedChild("value"), fixed.getValueElement(), fixedSource, "value", focus, pattern); + checkFixedValue(errors, path + ".period", focus.getNamedChild("period"), fixed.getPeriod(), fixedSource, "period", focus, pattern); + checkFixedValue(errors, path + ".assigner", focus.getNamedChild("assigner"), fixed.getAssigner(), fixedSource, "assigner", focus, pattern); + } + + private void checkPeriod(List errors, String path, Element focus, Period fixed, String fixedSource, boolean pattern) { + checkFixedValue(errors, path + ".start", focus.getNamedChild("start"), fixed.getStartElement(), fixedSource, "start", focus, pattern); + checkFixedValue(errors, path + ".end", focus.getNamedChild("end"), fixed.getEndElement(), fixedSource, "end", focus, pattern); + } + + private void checkPrimitive(Object appContext, List errors, String path, String type, ElementDefinition context, Element e, StructureDefinition profile, NodeStack node) throws FHIRException { + if (isBlank(e.primitiveValue())) { + if (e.primitiveValue() == null) + rule(errors, IssueType.INVALID, e.line(), e.col(), path, e.hasChildren(),messages.getString("Primitive_types_must_have_a_value_or_must_have_child_extensions")); + else if (e.primitiveValue().length() == 0) + rule(errors, IssueType.INVALID, e.line(), e.col(), path, e.hasChildren(),messages.getString("Primitive_types_must_have_a_value_that_is_not_empty")); + else if (StringUtils.isWhitespace(e.primitiveValue())) + warning(errors, IssueType.INVALID, e.line(), e.col(), path, e.hasChildren(),messages.getString("Primitive_types_should_not_only_be_whitespace")); + return; + } + String regex = context.getExtensionString(ToolingExtensions.EXT_REGEX); + if (regex != null) + rule(errors, IssueType.INVALID, e.line(), e.col(), path, e.primitiveValue().matches(regex),messages.getString("Element_value__does_not_meet_regex_"), e.primitiveValue(), regex); + + if (type.equals("boolean")) { + rule(errors, IssueType.INVALID, e.line(), e.col(), path, "true".equals(e.primitiveValue()) || "false".equals(e.primitiveValue()),messages.getString("boolean_values_must_be_true_or_false")); + } + if (type.equals("uri") || type.equals("oid") || type.equals("uuid") || type.equals("url") || type.equals("canonical")) { + String url = e.primitiveValue(); + rule(errors, IssueType.INVALID, e.line(), e.col(), path, !url.startsWith("oid:"),messages.getString("URI_values_cannot_start_with_oid")); + rule(errors, IssueType.INVALID, e.line(), e.col(), path, !url.startsWith("uuid:"),messages.getString("URI_values_cannot_start_with_uuid")); + rule(errors, IssueType.INVALID, e.line(), e.col(), path, url.equals(url.trim().replace(" ", "")) // work around an old invalid example in a core package || "http://www.acme.com/identifiers/patient or urn:ietf:rfc:3986 if the Identifier.value itself is a full uri".equals(url),messages.getString("URI_values_cannot_have_whitespace"), url); + rule(errors, IssueType.INVALID, e.line(), e.col(), path, !context.hasMaxLength() || context.getMaxLength() == 0 || url.length() <= context.getMaxLength(),messages.getString("value_is_longer_than_permitted_maximum_length_of_"), context.getMaxLength()); + rule(errors, IssueType.INVALID, e.line(), e.col(), path, !context.hasMaxLength() || context.getMaxLength() == 0 || e.primitiveValue().length() <= context.getMaxLength(),messages.getString("value_is_longer_than_permitted_maximum_length_of_"), context.getMaxLength()); + + if (type.equals("oid")) { + if (rule(errors, IssueType.INVALID, e.line(), e.col(), path, url.startsWith("urn:oid:"),messages.getString("OIDs_must_start_with_urnoid"))) + rule(errors, IssueType.INVALID, e.line(), e.col(), path, Utilities.isOid(url.substring(8)),messages.getString("OIDs_must_be_valid")); + } + if (type.equals("uuid")) { + rule(errors, IssueType.INVALID, e.line(), e.col(), path, url.startsWith("urn:uuid:"),messages.getString("UUIDs_must_start_with_urnuuid")); + try { + UUID.fromString(url.substring(8)); + } catch (Exception ex) { + rule(errors, IssueType.INVALID, e.line(), e.col(), path, false,messages.getString("UUIDs_must_be_valid_"), ex.getMessage()); + } + } + + // now, do we check the URI target? + if (fetcher != null) { + boolean found; + try { + found = isDefinitionURL(url) || (allowExamples && (url.contains("example.org") || url.contains("acme.com"))) || (url.startsWith("http://hl7.org/fhir/tools")) || fetcher.resolveURL(appContext, path, url); + } catch (IOException e1) { + found = false; + } + rule(errors, IssueType.INVALID, e.line(), e.col(), path, found,messages.getString("URL_value__does_not_resolve"), url); + } + } + if (type.equals("id")) { + // work around an old issue with ElementDefinition.id + if (!context.getPath().equals("ElementDefinition.id") && !VersionUtilities.versionsCompatible("1.4", this.context.getVersion())) { + rule(errors, IssueType.INVALID, e.line(), e.col(), path, FormatUtilities.isValidId(e.primitiveValue()),messages.getString("id_value__is_not_valid"), e.primitiveValue()); + } + } + if (type.equalsIgnoreCase("string") && e.hasPrimitiveValue()) { + if (rule(errors, IssueType.INVALID, e.line(), e.col(), path, e.primitiveValue() == null || e.primitiveValue().length() > 0,messages.getString("value_cannot_be_empty"))) { + warning(errors, IssueType.INVALID, e.line(), e.col(), path, e.primitiveValue() == null || e.primitiveValue().trim().equals(e.primitiveValue()),messages.getString("value_should_not_start_or_finish_with_whitespace")); + if (rule(errors, IssueType.INVALID, e.line(), e.col(), path, e.primitiveValue().length() <= 1048576,messages.getString("value_is_longer_than_permitted_maximum_length_of_1_MB_1048576_bytes"))) { + rule(errors, IssueType.INVALID, e.line(), e.col(), path, !context.hasMaxLength() || context.getMaxLength() == 0 || e.primitiveValue().length() <= context.getMaxLength(),messages.getString("value_is_longer_than_permitted_maximum_length_of_"), context.getMaxLength()); + } + } + } + if (type.equals("dateTime")) { + warning(errors, IssueType.INVALID, e.line(), e.col(), path, yearIsValid(e.primitiveValue()),messages.getString("The_value__is_outside_the_range_of_reasonable_years__check_for_data_entry_error"), e.primitiveValue()); + rule(errors, IssueType.INVALID, e.line(), e.col(), path, e.primitiveValue() .matches("([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)(-(0[1-9]|1[0-2])(-(0[1-9]|[1-2][0-9]|3[0-1])(T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]+)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00))?)?)?)?"),messages.getString("Not_a_valid_date_time")); + rule(errors, IssueType.INVALID, e.line(), e.col(), path, !hasTime(e.primitiveValue()) || hasTimeZone(e.primitiveValue()),messages.getString("if_a_date_has_a_time_it_must_have_a_timezone")); + rule(errors, IssueType.INVALID, e.line(), e.col(), path, !context.hasMaxLength() || context.getMaxLength() == 0 || e.primitiveValue().length() <= context.getMaxLength(),messages.getString("value_is_longer_than_permitted_maximum_length_of_"), context.getMaxLength()); + try { + DateTimeType dt = new DateTimeType(e.primitiveValue()); + } catch (Exception ex) { + rule(errors, IssueType.INVALID, e.line(), e.col(), path, false,messages.getString("Not_a_valid_datetime_"), ex.getMessage()); + } + } + if (type.equals("time")) { + rule(errors, IssueType.INVALID, e.line(), e.col(), path, e.primitiveValue() .matches("([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)"),messages.getString("Not_a_valid_time")); + try { + TimeType dt = new TimeType(e.primitiveValue()); + } catch (Exception ex) { + rule(errors, IssueType.INVALID, e.line(), e.col(), path, false,messages.getString("Not_a_valid_time_"), ex.getMessage()); + } + } + if (type.equals("date")) { + warning(errors, IssueType.INVALID, e.line(), e.col(), path, yearIsValid(e.primitiveValue()),messages.getString("The_value__is_outside_the_range_of_reasonable_years__check_for_data_entry_error"), e.primitiveValue()); + rule(errors, IssueType.INVALID, e.line(), e.col(), path, e.primitiveValue().matches("([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)(-(0[1-9]|1[0-2])(-(0[1-9]|[1-2][0-9]|3[0-1]))?)?"),messages.getString("Not_a_valid_date")); + rule(errors, IssueType.INVALID, e.line(), e.col(), path, !context.hasMaxLength() || context.getMaxLength() == 0 || e.primitiveValue().length() <= context.getMaxLength(),messages.getString("value_is_longer_than_permitted_maximum_value_of_"), context.getMaxLength()); + try { + DateType dt = new DateType(e.primitiveValue()); + } catch (Exception ex) { + rule(errors, IssueType.INVALID, e.line(), e.col(), path, false,messages.getString("Not_a_valid_date_"), ex.getMessage()); + } + } + if (type.equals("base64Binary")) { + String encoded = e.primitiveValue(); + if (isNotBlank(encoded)) { + /* + * Technically this is not bulletproof as some invalid base64 won't be caught, + * but I think it's good enough. The original code used Java8 Base64 decoder + * but I've replaced it with a regex for 2 reasons: + * 1. This code will run on any version of Java + * 2. This code doesn't actually decode, which is much easier on memory use for big payloads + */ + int charCount = 0; + for (int i = 0; i < encoded.length(); i++) { + char nextChar = encoded.charAt(i); + if (Character.isWhitespace(nextChar)) { + continue; + } + if (Character.isLetterOrDigit(nextChar)) { + charCount++; + } + if (nextChar == '/' || nextChar == '=' || nextChar == '+') { + charCount++; + } + } + + if (charCount > 0 && charCount % 4 != 0) { + String value = encoded.length() < 100 ? encoded : "(snip)"; + rule(errors, IssueType.INVALID, e.line(), e.col(), path, false,messages.getString("The_value_0_is_not_a_valid_Base64_value"), value); + } + } + } + if (type.equals("integer") || type.equals("unsignedInt") || type.equals("positiveInt")) { + if (rule(errors, IssueType.INVALID, e.line(), e.col(), path, Utilities.isInteger(e.primitiveValue()),messages.getString("The_value__is_not_a_valid_integer"), e.primitiveValue())) { + Integer v = new Integer(e.getValue()).intValue(); + rule(errors, IssueType.INVALID, e.line(), e.col(), path, !context.hasMaxValueIntegerType() || !context.getMaxValueIntegerType().hasValue() || (context.getMaxValueIntegerType().getValue() >= v),messages.getString("value_is_greater_than_permitted_maximum_value_of_"), (context.hasMaxValueIntegerType() ? context.getMaxValueIntegerType() : "")); + rule(errors, IssueType.INVALID, e.line(), e.col(), path, !context.hasMinValueIntegerType() || !context.getMinValueIntegerType().hasValue() || (context.getMinValueIntegerType().getValue() <= v),messages.getString("value_is_less_than_permitted_minimum_value_of_"), (context.hasMinValueIntegerType() ? context.getMinValueIntegerType() : "")); + if (type.equals("unsignedInt")) + rule(errors, IssueType.INVALID, e.line(), e.col(), path, v >= 0,messages.getString("value_is_less_than_permitted_minimum_value_of_0")); + if (type.equals("positiveInt")) + rule(errors, IssueType.INVALID, e.line(), e.col(), path, v > 0,messages.getString("value_is_less_than_permitted_minimum_value_of_1")); + } + } + if (type.equals("integer64")) { + if (rule(errors, IssueType.INVALID, e.line(), e.col(), path, Utilities.isLong(e.primitiveValue()),messages.getString("The_value__is_not_a_valid_integer64"), e.primitiveValue())) { + Long v = new Long(e.getValue()).longValue(); + rule(errors, IssueType.INVALID, e.line(), e.col(), path, !context.hasMaxValueInteger64Type() || !context.getMaxValueInteger64Type().hasValue() || (context.getMaxValueInteger64Type().getValue() >= v),messages.getString("value_is_greater_than_permitted_maximum_value_of_"), (context.hasMaxValueInteger64Type() ? context.getMaxValueInteger64Type() : "")); + rule(errors, IssueType.INVALID, e.line(), e.col(), path, !context.hasMinValueInteger64Type() || !context.getMinValueInteger64Type().hasValue() || (context.getMinValueInteger64Type().getValue() <= v),messages.getString("value_is_less_than_permitted_minimum_value_of_"), (context.hasMinValueInteger64Type() ? context.getMinValueInteger64Type() : "")); + if (type.equals("unsignedInt")) + rule(errors, IssueType.INVALID, e.line(), e.col(), path, v >= 0,messages.getString("value_is_less_than_permitted_minimum_value_of_0")); + if (type.equals("positiveInt")) + rule(errors, IssueType.INVALID, e.line(), e.col(), path, v > 0,messages.getString("value_is_less_than_permitted_minimum_value_of_1")); + } + } + if (type.equals("decimal")) { + if (e.primitiveValue() != null) { + DecimalStatus ds = Utilities.checkDecimal(e.primitiveValue(), true, false); + if (rule(errors, IssueType.INVALID, e.line(), e.col(), path, ds == DecimalStatus.OK || ds == DecimalStatus.RANGE,messages.getString("The_value__is_not_a_valid_decimal"), e.primitiveValue())) + warning(errors, IssueType.VALUE, e.line(), e.col(), path, ds != DecimalStatus.RANGE,messages.getString("The_value__is_outside_the_range_of_commonlyreasonably_supported_decimals"), e.primitiveValue()); + } + } + if (type.equals("instant")) { + rule(errors, IssueType.INVALID, e.line(), e.col(), path, e.primitiveValue().matches("-?[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]+)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00))"),messages.getString("The_instant__is_not_valid_by_regex"), e.primitiveValue()); + warning(errors, IssueType.INVALID, e.line(), e.col(), path, yearIsValid(e.primitiveValue()),messages.getString("The_value__is_outside_the_range_of_reasonable_years__check_for_data_entry_error"), e.primitiveValue()); + try { + InstantType dt = new InstantType(e.primitiveValue()); + } catch (Exception ex) { + rule(errors, IssueType.INVALID, e.line(), e.col(), path, false,messages.getString("Not_a_valid_instant_"), ex.getMessage()); + } + } + + if (type.equals("code") && e.primitiveValue() != null) { + // Technically, a code is restricted to string which has at least one character and no leading or trailing whitespace, and where there is no whitespace + // other than single spaces in the contents + rule(errors, IssueType.INVALID, e.line(), e.col(), path, passesCodeWhitespaceRules(e.primitiveValue()),messages.getString("The_code__is_not_valid_whitespace_rules"), e.primitiveValue()); + rule(errors, IssueType.INVALID, e.line(), e.col(), path, !context.hasMaxLength() || context.getMaxLength() == 0 || e.primitiveValue().length() <= context.getMaxLength(),messages.getString("value_is_longer_than_permitted_maximum_length_of_"), context.getMaxLength()); + } + + if (context.hasBinding() && e.primitiveValue() != null) { + checkPrimitiveBinding(errors, path, type, context, e, profile, node); + } + + if (type.equals("xhtml")) { + XhtmlNode xhtml = e.getXhtml(); + if (xhtml != null) { // if it is null, this is an error already noted in the parsers + // check that the namespace is there and correct. + String ns = xhtml.getNsDecl(); + rule(errors, IssueType.INVALID, e.line(), e.col(), path, FormatUtilities.XHTML_NS.equals(ns),messages.getString("Wrong_namespace_on_the_XHTML__should_be_"), ns, FormatUtilities.XHTML_NS); + // check that inner namespaces are all correct + checkInnerNS(errors, e, path, xhtml.getChildNodes()); + rule(errors, IssueType.INVALID, e.line(), e.col(), path, "div".equals(xhtml.getName()),messages.getString("Wrong_name_on_the_XHTML___must_start_with_div"), ns); + // check that no illegal elements and attributes have been used + checkInnerNames(errors, e, path, xhtml.getChildNodes()); + } + } + + if (context.hasFixed()) { + checkFixedValue(errors, path, e, context.getFixed(), profile.getUrl(), context.getSliceName(), null, false); + } + if (context.hasPattern()) { + checkFixedValue(errors, path, e, context.getPattern(), profile.getUrl(), context.getSliceName(), null, true); + } + + // for nothing to check + } + + private boolean isDefinitionURL(String url) { + return Utilities.existsInList(url, "http://hl7.org/fhirpath/System.Boolean", "http://hl7.org/fhirpath/System.String", "http://hl7.org/fhirpath/System.Integer", + "http://hl7.org/fhirpath/System.Decimal", "http://hl7.org/fhirpath/System.Date", "http://hl7.org/fhirpath/System.Time", "http://hl7.org/fhirpath/System.DateTime", "http://hl7.org/fhirpath/System.Quantity"); + } + + private void checkInnerNames(List errors, Element e, String path, List list) { + for (XhtmlNode node : list) { + if (node.getNodeType() == NodeType.Element) { + rule(errors, IssueType.INVALID, e.line(), e.col(), path, Utilities.existsInList(node.getName(), "p", "br", "div", "h1", "h2", "h3", "h4", "h5", "h6", "a", "span", "b", "em", "i", "strong", "small", "big", "tt", "small", "dfn", "q", "var", "abbr", "acronym", "cite", "blockquote", "hr", "address", "bdo", "kbd", "q", "sub", "sup", "ul", "ol", "li", "dl", "dt", "dd", "pre", "table", "caption", "colgroup", "col", "thead", "tr", "tfoot", "tbody", "th", "td", "code", "samp", "img", "map", "area" ),messages.getString("Illegal_element_name_in_the_XHTML_"), node.getName()); + for (String an : node.getAttributes().keySet()) { + boolean ok = an.startsWith("xmlns") || Utilities.existsInList(an, + "title", "style", "class", "id", "lang", "xml:lang", "dir", "accesskey", "tabindex", + // tables + "span", "width", "align", "valign", "char", "charoff", "abbr", "axis", "headers", "scope", "rowspan", "colspan") || + + Utilities.existsInList(node.getName() + "." + an, "a.href", "a.name", "img.src", "img.border", "div.xmlns", "blockquote.cite", "q.cite", + "a.charset", "a.type", "a.name", "a.href", "a.hreflang", "a.rel", "a.rev", "a.shape", "a.coords", "img.src", + "img.alt", "img.longdesc", "img.height", "img.width", "img.usemap", "img.ismap", "map.name", "area.shape", + "area.coords", "area.href", "area.nohref", "area.alt", "table.summary", "table.width", "table.border", + "table.frame", "table.rules", "table.cellspacing", "table.cellpadding", "pre.space", "td.nowrap" + ); + if (!ok) + rule(errors, IssueType.INVALID, e.line(), e.col(), path, false,messages.getString("Illegal_attribute_name_in_the_XHTML__on_"), an, node.getName()); + } + checkInnerNames(errors, e, path, node.getChildNodes()); + } + } + } + + private void checkInnerNS(List errors, Element e, String path, List list) { + for (XhtmlNode node : list) { + if (node.getNodeType() == NodeType.Element) { + String ns = node.getNsDecl(); + rule(errors, IssueType.INVALID, e.line(), e.col(), path, ns == null || FormatUtilities.XHTML_NS.equals(ns),messages.getString("Wrong_namespace_on_the_XHTML__should_be_"), ns, FormatUtilities.XHTML_NS); + checkInnerNS(errors, e, path, node.getChildNodes()); + } + } + } + + private void checkPrimitiveBinding(List errors, String path, String type, ElementDefinition elementContext, Element element, StructureDefinition profile, NodeStack stack) { + // We ignore bindings that aren't on string, uri or code + if (!element.hasPrimitiveValue() || !("code".equals(type) || "string".equals(type) || "uri".equals(type) || "url".equals(type) || "canonical".equals(type))) { + return; + } + if (noTerminologyChecks) + return; + + String value = element.primitiveValue(); + // System.out.println("check "+value+" in "+path); + + // firstly, resolve the value set + ElementDefinitionBindingComponent binding = elementContext.getBinding(); + if (binding.hasValueSet()) { + ValueSet vs = resolveBindingReference(profile, binding.getValueSet(), profile.getUrl()); + if (warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, vs != null,messages.getString("ValueSet_0_not_found_by_validator"), describeReference(binding.getValueSet()))) { + long t = System.nanoTime(); + ValidationResult vr = null; + if (binding.getStrength() != BindingStrength.EXAMPLE) { + vr = context.validateCode(new ValidationOptions(stack.workingLang), value, vs); + } + txTime = txTime + (System.nanoTime() - t); + if (vr != null && !vr.isOk()) { + if (vr.IsNoService()) + txHint(errors, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false,messages.getString("The_value_provided__could_not_be_validated_in_the_absence_of_a_terminology_server"), value); + else if (binding.getStrength() == BindingStrength.REQUIRED) + txRule(errors, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false, "The value provided ('" + value + "') is not in the value set " + describeReference(binding.getValueSet()) + " (" + vs.getUrl() + ", and a code is required from this value set)" + getErrorMessage(vr.getMessage())); + else if (binding.getStrength() == BindingStrength.EXTENSIBLE) { + if (binding.hasExtension("http://hl7.org/fhir/StructureDefinition/elementdefinition-maxValueSet")) + checkMaxValueSet(errors, path, element, profile, ToolingExtensions.readStringExtension(binding, "http://hl7.org/fhir/StructureDefinition/elementdefinition-maxValueSet"), value, stack); + else if (!noExtensibleWarnings) + txWarning(errors, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false,messages.getString("The_value_provided__is_not_in_the_value_set___and_a_code_should_come_from_this_value_set_unless_it_has_no_suitable_code"), value, describeReference(binding.getValueSet()), vs.getUrl(), getErrorMessage(vr.getMessage())); + } else if (binding.getStrength() == BindingStrength.PREFERRED) { + if (baseOnly) { + txHint(errors, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false,messages.getString("The_value_provided__is_not_in_the_value_set___and_a_code_is_recommended_to_come_from_this_value_set"), value, describeReference(binding.getValueSet()), vs.getUrl(), getErrorMessage(vr.getMessage())); + } + } + } + } + } else if (!noBindingMsgSuppressed) + hint(errors, IssueType.CODEINVALID, element.line(), element.col(), path, !type.equals("code"),messages.getString("Binding_has_no_source_so_cant_be_checked")); + } + + private void checkQuantity(List errors, String path, Element focus, Quantity fixed, String fixedSource, boolean pattern) { + checkFixedValue(errors, path + ".value", focus.getNamedChild("value"), fixed.getValueElement(), fixedSource, "value", focus, pattern); + checkFixedValue(errors, path + ".comparator", focus.getNamedChild("comparator"), fixed.getComparatorElement(), fixedSource, "comparator", focus, pattern); + checkFixedValue(errors, path + ".units", focus.getNamedChild("unit"), fixed.getUnitElement(), fixedSource, "units", focus, pattern); + checkFixedValue(errors, path + ".system", focus.getNamedChild("system"), fixed.getSystemElement(), fixedSource, "system", focus, pattern); + checkFixedValue(errors, path + ".code", focus.getNamedChild("code"), fixed.getCodeElement(), fixedSource, "code", focus, pattern); + } + + // implementation + + private void checkRange(List errors, String path, Element focus, Range fixed, String fixedSource, boolean pattern) { + checkFixedValue(errors, path + ".low", focus.getNamedChild("low"), fixed.getLow(), fixedSource, "low", focus, pattern); + checkFixedValue(errors, path + ".high", focus.getNamedChild("high"), fixed.getHigh(), fixedSource, "high", focus, pattern); + + } + + private void checkRatio(List errors, String path, Element focus, Ratio fixed, String fixedSource, boolean pattern) { + checkFixedValue(errors, path + ".numerator", focus.getNamedChild("numerator"), fixed.getNumerator(), fixedSource, "numerator", focus, pattern); + checkFixedValue(errors, path + ".denominator", focus.getNamedChild("denominator"), fixed.getDenominator(), fixedSource, "denominator", focus, pattern); + } + + private void checkReference(ValidatorHostContext hostContext, List errors, String path, Element element, StructureDefinition profile, ElementDefinition container, String parentType, NodeStack stack) throws FHIRException { + Reference reference = ObjectConverter.readAsReference(element); + + String ref = reference.getReference(); + if (Utilities.noString(ref)) { + if (Utilities.noString(reference.getIdentifier().getSystem()) && Utilities.noString(reference.getIdentifier().getValue())) { + warning(errors, IssueType.STRUCTURE, element.line(), element.col(), path, !Utilities.noString(element.getNamedChildValue("display")),messages.getString("A_Reference_without_an_actual_reference_or_identifier_should_have_a_display")); + } + return; + } else if (Utilities.existsInList(ref, "http://tools.ietf.org/html/bcp47")) { + // special known URLs that can't be validated but are known to be valid + return; + } + + ResolvedReference we = localResolve(ref, stack, errors, path, (Element) hostContext.getAppContext(), element); + String refType; + if (ref.startsWith("#")) { + refType = "contained"; + } else { + if (we == null) { + refType = "remote"; + } else { + refType = "bundled"; + } + } + ReferenceValidationPolicy pol = refType.equals("contained") || refType.equals("bundled") ? ReferenceValidationPolicy.CHECK_VALID : fetcher == null ? ReferenceValidationPolicy.IGNORE : fetcher.validationPolicy(hostContext.getAppContext(), path, ref); + + if (pol.checkExists()) { + if (we == null) { + if (fetcher == null) { + if (!refType.equals("contained")) + throw new FHIRException("Resource resolution services not provided"); + } else { + Element ext = null; + if (fetchCache.containsKey(ref)) { + ext = fetchCache.get(ref); + } else { + try { + ext = fetcher.fetch(hostContext.getAppContext(), ref); + } catch (IOException e) { + throw new FHIRException(e); + } + if (ext != null) { + fetchCache.put(ref, ext); + } + } + we = ext == null ? null : makeExternalRef(ext, path); + } + } + rule(errors, IssueType.STRUCTURE, element.line(), element.col(), path, (allowExamples && (ref.contains("example.org") || ref.contains("acme.com"))) || (we != null || pol == ReferenceValidationPolicy.CHECK_TYPE_IF_EXISTS),messages.getString("Unable_to_resolve_resource_"), ref); + } + + String ft; + if (we != null) + ft = we.getType(); + else + ft = tryParse(ref); + + if (reference.hasType()) { // R4 onwards... + // the type has to match the specified + String tu = isAbsolute(reference.getType()) ? reference.getType() : "http://hl7.org/fhir/StructureDefinition/" + reference.getType(); + TypeRefComponent containerType = container.getType("Reference"); + if (!containerType.hasTargetProfile(tu) && !containerType.hasTargetProfile("http://hl7.org/fhir/StructureDefinition/Resource")) { + boolean matchingResource = false; + for (CanonicalType target : containerType.getTargetProfile()) { + StructureDefinition sd = resolveProfile(profile, target.asStringValue()); + if (("http://hl7.org/fhir/StructureDefinition/" + sd.getType()).equals(tu)) { + matchingResource = true; + break; + } + } + rule(errors, IssueType.STRUCTURE, element.line(), element.col(), path, matchingResource,messages.getString("The_type__is_not_a_valid_Target_for_this_element_must_be_one_of_"), reference.getType(), container.getType("Reference").getTargetProfile()); + + } + // the type has to match the actual + rule(errors, IssueType.STRUCTURE, element.line(), element.col(), path, ft == null || ft.equals(reference.getType()),messages.getString("The_specified_type__does_not_match_the_found_type_"), reference.getType(), ft); + } + + if (we != null && pol.checkType()) { + if (warning(errors, IssueType.STRUCTURE, element.line(), element.col(), path, ft != null,messages.getString("Unable_to_determine_type_of_target_resource"))) { + // we validate as much as we can. First, can we infer a type from the profile? + boolean ok = false; + TypeRefComponent type = getReferenceTypeRef(container.getType()); + if (type.hasTargetProfile() && !type.hasTargetProfile("http://hl7.org/fhir/StructureDefinition/Resource")) { + Set types = new HashSet<>(); + List profiles = new ArrayList<>(); + for (UriType u : type.getTargetProfile()) { + StructureDefinition sd = resolveProfile(profile, u.getValue()); + if (rule(errors, IssueType.STRUCTURE, element.line(), element.col(), path, sd != null,messages.getString("Unable_to_resolve_the_profile_reference_"), u.getValue())) { + types.add(sd.getType()); + if (ft.equals(sd.getType())) { + ok = true; + profiles.add(sd); + } + } + } + if (!pol.checkValid()) { + rule(errors, IssueType.STRUCTURE, element.line(), element.col(), path, profiles.size() > 0,messages.getString("Unable_to_find_matching_profile_for__by_type_among_choices_"), ref, StringUtils.join("; ", type.getTargetProfile())); + } else { + Map> badProfiles = new HashMap>(); + Map> goodProfiles = new HashMap>(); + int goodCount = 0; + for (StructureDefinition pr : profiles) { + List profileErrors = new ArrayList(); + validateResource(we.hostContext(hostContext, pr), profileErrors, we.getResource(), we.getFocus(), pr, IdStatus.OPTIONAL, we.getStack()); + if (!hasErrors(profileErrors)) { + goodCount++; + goodProfiles.put(pr, profileErrors); + trackUsage(pr, hostContext, element); + } else { + badProfiles.put(pr, profileErrors); + } + } + if (goodCount == 1) { + if (showMessagesFromReferences) { + for (ValidationMessage vm : goodProfiles.values().iterator().next()) { + if (!errors.contains(vm)) { + errors.add(vm); + } + } + } + + } else if (goodProfiles.size() == 0) { + if (!isShowMessagesFromReferences()) { + rule(errors, IssueType.STRUCTURE, element.line(), element.col(), path, areAllBaseProfiles(profiles),messages.getString("Unable_to_find_matching_profile_for__among_choices_"), ref, asList(type.getTargetProfile())); + for (StructureDefinition sd : badProfiles.keySet()) { + slicingHint(errors, IssueType.STRUCTURE, element.line(), element.col(), path, false, "Details for " + ref + " matching against Profile" + sd.getUrl(), errorSummaryForSlicingAsHtml(badProfiles.get(sd))); + } + } else { + rule(errors, IssueType.STRUCTURE, element.line(), element.col(), path, profiles.size() == 1,messages.getString("Unable_to_find_matching_profile_for__among_choices_"), ref, asList(type.getTargetProfile())); + for (List messages : badProfiles.values()) { + for (ValidationMessage vm : messages) { + if (!errors.contains(vm)) { + errors.add(vm); + } + } + } + } + } else { + if (!isShowMessagesFromReferences()) { + warning(errors, IssueType.STRUCTURE, element.line(), element.col(), path, false,messages.getString("Found_multiple_matching_profiles_for__among_choices_"), ref, asListByUrl(goodProfiles.keySet())); + for (StructureDefinition sd : badProfiles.keySet()) { + slicingHint(errors, IssueType.STRUCTURE, element.line(), element.col(), path, false, "Details for " + ref + " matching against Profile" + sd.getUrl(), errorSummaryForSlicingAsHtml(badProfiles.get(sd))); + } + } else { + warning(errors, IssueType.STRUCTURE, element.line(), element.col(), path, false,messages.getString("Found_multiple_matching_profiles_for__among_choices_"), ref, asListByUrl(goodProfiles.keySet())); + for (List messages : goodProfiles.values()) { + for (ValidationMessage vm : messages) { + if (!errors.contains(vm)) { + errors.add(vm); + } + } + } + } + } + } + rule(errors, IssueType.STRUCTURE, element.line(), element.col(), path, ok,messages.getString("Invalid_Resource_target_type_Found__but_expected_one_of_"), ft, types.toString()); + } + if (type.hasAggregation()) { + boolean modeOk = false; + for (Enumeration mode : type.getAggregation()) { + if (mode.getValue().equals(AggregationMode.CONTAINED) && refType.equals("contained")) + modeOk = true; + else if (mode.getValue().equals(AggregationMode.BUNDLED) && refType.equals("bundled")) + modeOk = true; + else if (mode.getValue().equals(AggregationMode.REFERENCED) && (refType.equals("bundled") || refType.equals("remote"))) + modeOk = true; + } + rule(errors, IssueType.STRUCTURE, element.line(), element.col(), path, modeOk,messages.getString("Reference_is__which_isnt_supported_by_the_specified_aggregation_modes_for_the_reference"), refType); + } + } + } + if (we == null) { + TypeRefComponent type = getReferenceTypeRef(container.getType()); + boolean okToRef = !type.hasAggregation() || type.hasAggregation(AggregationMode.REFERENCED); + rule(errors, IssueType.REQUIRED, -1, -1, path, okToRef,messages.getString("Bundled_or_contained_reference_not_found_within_the_bundleresource_"), ref); + } + if (we == null && ft != null && assumeValidRestReferences) { + // if we == null, we inferred ft from the reference. if we are told to treat this as gospel + TypeRefComponent type = getReferenceTypeRef(container.getType()); + Set types = new HashSet<>(); + for (CanonicalType tp : type.getTargetProfile()) { + StructureDefinition sd = context.fetchResource(StructureDefinition.class, tp.getValue()); + if (sd != null) { + types.add(sd.getType()); + } + } + rule(errors, IssueType.STRUCTURE, element.line(), element.col(), path, types.isEmpty() || types.contains(ft),messages.getString("The_type__implied_by_the_reference_URL__is_not_a_valid_Target_for_this_element_must_be_one_of_"), ft, ref, types); + + } + if (pol == ReferenceValidationPolicy.CHECK_VALID) { + // todo.... + } + } + + private String asListByUrl(Collection list) { + CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder(); + for (StructureDefinition sd : list) { + b.append(sd.getUrl()); + } + return b.toString(); + } + + private String asList(Collection list) { + CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder(); + for (CanonicalType c : list) { + b.append(c.getValue()); + } + return b.toString(); + } + + private boolean areAllBaseProfiles(List profiles) { + for (StructureDefinition sd : profiles) { + if (!sd.getUrl().startsWith("http://hl7.org/fhir/StructureDefinition/")) { + return false; + } + } + return true; + } + + private String errorSummaryForSlicing(List list) { + CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder(); + for (ValidationMessage vm : list) { + if (vm.getLevel() == IssueSeverity.ERROR || vm.getLevel() == IssueSeverity.FATAL || vm.isSlicingHint()) { + b.append(vm.getLocation() + ": " + vm.getMessage()); + } + } + return b.toString(); + } + + private String errorSummaryForSlicingAsHtml(List list) { + CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder(); + for (ValidationMessage vm : list) { + if (vm.isSlicingHint()) { + b.append("
  • " + vm.getLocation() + ": " + vm.getSliceHtml() + "
  • "); + } else if (vm.getLevel() == IssueSeverity.ERROR || vm.getLevel() == IssueSeverity.FATAL) { + b.append("
  • " + vm.getLocation() + ": " + vm.getHtml() + "
  • "); + } + } + return "
      " + b.toString() + "
    "; + } + + private TypeRefComponent getReferenceTypeRef(List types) { + for (TypeRefComponent tr : types) { + if ("Reference".equals(tr.getCode())) { + return tr; + } + } + return null; + } + + private String checkResourceType(String type) { + long t = System.nanoTime(); + try { + if (context.fetchResource(StructureDefinition.class, "http://hl7.org/fhir/StructureDefinition/" + type) != null) + return type; + else + return null; + } finally { + sdTime = sdTime + (System.nanoTime() - t); + } + } + + private void checkSampledData(List errors, String path, Element focus, SampledData fixed, String fixedSource, boolean pattern) { + checkFixedValue(errors, path + ".origin", focus.getNamedChild("origin"), fixed.getOrigin(), fixedSource, "origin", focus, pattern); + checkFixedValue(errors, path + ".period", focus.getNamedChild("period"), fixed.getPeriodElement(), fixedSource, "period", focus, pattern); + checkFixedValue(errors, path + ".factor", focus.getNamedChild("factor"), fixed.getFactorElement(), fixedSource, "factor", focus, pattern); + checkFixedValue(errors, path + ".lowerLimit", focus.getNamedChild("lowerLimit"), fixed.getLowerLimitElement(), fixedSource, "lowerLimit", focus, pattern); + checkFixedValue(errors, path + ".upperLimit", focus.getNamedChild("upperLimit"), fixed.getUpperLimitElement(), fixedSource, "upperLimit", focus, pattern); + checkFixedValue(errors, path + ".dimensions", focus.getNamedChild("dimensions"), fixed.getDimensionsElement(), fixedSource, "dimensions", focus, pattern); + checkFixedValue(errors, path + ".data", focus.getNamedChild("data"), fixed.getDataElement(), fixedSource, "data", focus, pattern); + } + + private void checkTiming(List errors, String path, Element focus, Timing fixed, String fixedSource, boolean pattern) { + checkFixedValue(errors, path + ".repeat", focus.getNamedChild("repeat"), fixed.getRepeat(), fixedSource, "value", focus, pattern); + + List events = new ArrayList(); + focus.getNamedChildren("event", events); + if (rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, events.size() == fixed.getEvent().size(),messages.getString("Expected__but_found__event_elements"), Integer.toString(fixed.getEvent().size()), Integer.toString(events.size()))) { + for (int i = 0; i < events.size(); i++) + checkFixedValue(errors, path + ".event", events.get(i), fixed.getEvent().get(i), fixedSource, "event", focus, pattern); + } + } + + private boolean codeinExpansion(ValueSetExpansionContainsComponent cnt, String system, String code) { + for (ValueSetExpansionContainsComponent c : cnt.getContains()) { + if (code.equals(c.getCode()) && system.equals(c.getSystem().toString())) + return true; + if (codeinExpansion(c, system, code)) return true; } + return false; + } - private String errorSummaryForSlicing(List list) { - CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder(); - for (ValidationMessage vm : list) { - if (vm.getLevel() == IssueSeverity.ERROR || vm.getLevel() == IssueSeverity.FATAL || vm.isSlicingHint()) { - b.append(vm.getLocation() + ": " + vm.getMessage()); - } + private boolean codeInExpansion(ValueSet vs, String system, String code) { + for (ValueSetExpansionContainsComponent c : vs.getExpansion().getContains()) { + if (code.equals(c.getCode()) && (system == null || system.equals(c.getSystem()))) + return true; + if (codeinExpansion(c, system, code)) + return true; + } + return false; + } + + private String describeReference(String reference) { + if (reference == null) + return "null"; + return reference; + } + + private String describeReference(String reference, CanonicalResource target) { + if (reference == null && target == null) + return "null"; + if (reference == null) { + return target.getUrl(); + } + if (target == null) { + return reference; + } + if (reference.equals(target.getUrl())) { + return reference; + } + return reference + "(which actually refers to " + target.getUrl() + ")"; + } + + private String describeTypes(List types) { + CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder(); + for (TypeRefComponent t : types) { + b.append(t.getWorkingCode()); + } + return b.toString(); + } + + protected ElementDefinition findElement(StructureDefinition profile, String name) { + for (ElementDefinition c : profile.getSnapshot().getElement()) { + if (c.getPath().equals(name)) { + return c; + } + } + return null; + } + + public BestPracticeWarningLevel getBestPracticeWarningLevel() { + return bpWarnings; + } + + @Override + public CheckDisplayOption getCheckDisplay() { + return checkDisplay; + } + + private ConceptDefinitionComponent getCodeDefinition(ConceptDefinitionComponent c, String code) { + if (code.equals(c.getCode())) + return c; + for (ConceptDefinitionComponent g : c.getConcept()) { + ConceptDefinitionComponent r = getCodeDefinition(g, code); + if (r != null) + return r; + } + return null; + } + + private ConceptDefinitionComponent getCodeDefinition(CodeSystem cs, String code) { + for (ConceptDefinitionComponent c : cs.getConcept()) { + ConceptDefinitionComponent r = getCodeDefinition(c, code); + if (r != null) + return r; + } + return null; + } + + private IndexedElement getContainedById(Element container, String id) { + List contained = new ArrayList(); + container.getNamedChildren("contained", contained); + for (int i = 0; i < contained.size(); i++) { + Element we = contained.get(i); + if (id.equals(we.getNamedChildValue("id"))) { + return new IndexedElement(i, we, null); + } + } + return null; + } + + public IWorkerContext getContext() { + return context; + } + + private List getCriteriaForDiscriminator(String path, ElementDefinition element, String discriminator, StructureDefinition profile, boolean removeResolve) throws FHIRException { + List elements = new ArrayList(); + if ("value".equals(discriminator) && element.hasFixed()) { + elements.add(element); + return elements; + } + + if (removeResolve) { // if we're doing profile slicing, we don't want to walk into the last resolve.. we need the profile on the source not the target + if (discriminator.equals("resolve()")) { + elements.add(element); + return elements; + } + if (discriminator.endsWith(".resolve()")) + discriminator = discriminator.substring(0, discriminator.length() - 10); + } + + ElementDefinition ed = null; + ExpressionNode expr = fpe.parse(fixExpr(discriminator)); + long t2 = System.nanoTime(); + ed = fpe.evaluateDefinition(expr, profile, element); + sdTime = sdTime + (System.nanoTime() - t2); + if (ed != null) + elements.add(ed); + + for (TypeRefComponent type : element.getType()) { + for (CanonicalType p : type.getProfile()) { + String id = p.hasExtension(ToolingExtensions.EXT_PROFILE_ELEMENT) ? p.getExtensionString(ToolingExtensions.EXT_PROFILE_ELEMENT) : null; + StructureDefinition sd = context.fetchResource(StructureDefinition.class, p.getValue()); + if (sd == null) + throw new DefinitionException("Unable to resolve profile " + p); + profile = sd; + if (id == null) + element = sd.getSnapshot().getElementFirstRep(); + else { + element = null; + for (ElementDefinition t : sd.getSnapshot().getElement()) { + if (id.equals(t.getId())) + element = t; + } + if (element == null) + throw new DefinitionException("Unable to resolve element " + id + " in profile " + p); } - return b.toString(); - } - - private String errorSummaryForSlicingAsHtml(List list) { - CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder(); - for (ValidationMessage vm : list) { - if (vm.isSlicingHint()) { - b.append("
  • " + vm.getLocation() + ": " + vm.getSliceHtml() + "
  • "); - } else if (vm.getLevel() == IssueSeverity.ERROR || vm.getLevel() == IssueSeverity.FATAL) { - b.append("
  • " + vm.getLocation() + ": " + vm.getHtml() + "
  • "); - } - } - return "
      " + b.toString() + "
    "; - } - - private TypeRefComponent getReferenceTypeRef(List types) { - for (TypeRefComponent tr : types) { - if ("Reference".equals(tr.getCode())) { - return tr; - } - } - return null; - } - - private String checkResourceType(String type) { - long t = System.nanoTime(); - try { - if (context.fetchResource(StructureDefinition.class, "http://hl7.org/fhir/StructureDefinition/" + type) != null) - return type; - else - return null; - } finally { - sdTime = sdTime + (System.nanoTime() - t); - } - } - - private void checkSampledData(List errors, String path, Element focus, SampledData fixed, String fixedSource, boolean pattern) { - checkFixedValue(errors, path + ".origin", focus.getNamedChild("origin"), fixed.getOrigin(), fixedSource, "origin", focus, pattern); - checkFixedValue(errors, path + ".period", focus.getNamedChild("period"), fixed.getPeriodElement(), fixedSource, "period", focus, pattern); - checkFixedValue(errors, path + ".factor", focus.getNamedChild("factor"), fixed.getFactorElement(), fixedSource, "factor", focus, pattern); - checkFixedValue(errors, path + ".lowerLimit", focus.getNamedChild("lowerLimit"), fixed.getLowerLimitElement(), fixedSource, "lowerLimit", focus, pattern); - checkFixedValue(errors, path + ".upperLimit", focus.getNamedChild("upperLimit"), fixed.getUpperLimitElement(), fixedSource, "upperLimit", focus, pattern); - checkFixedValue(errors, path + ".dimensions", focus.getNamedChild("dimensions"), fixed.getDimensionsElement(), fixedSource, "dimensions", focus, pattern); - checkFixedValue(errors, path + ".data", focus.getNamedChild("data"), fixed.getDataElement(), fixedSource, "data", focus, pattern); - } - - private void checkTiming(List errors, String path, Element focus, Timing fixed, String fixedSource, boolean pattern) { - checkFixedValue(errors, path + ".repeat", focus.getNamedChild("repeat"), fixed.getRepeat(), fixedSource, "value", focus, pattern); - - List events = new ArrayList(); - focus.getNamedChildren("event", events); - if (rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, events.size() == fixed.getEvent().size(), - "Expected " + Integer.toString(fixed.getEvent().size()) + " but found " + Integer.toString(events.size()) + " event elements")) { - for (int i = 0; i < events.size(); i++) - checkFixedValue(errors, path + ".event", events.get(i), fixed.getEvent().get(i), fixedSource, "event", focus, pattern); - } - } - - private boolean codeinExpansion(ValueSetExpansionContainsComponent cnt, String system, String code) { - for (ValueSetExpansionContainsComponent c : cnt.getContains()) { - if (code.equals(c.getCode()) && system.equals(c.getSystem().toString())) - return true; - if (codeinExpansion(c, system, code)) - return true; - } - return false; - } - - private boolean codeInExpansion(ValueSet vs, String system, String code) { - for (ValueSetExpansionContainsComponent c : vs.getExpansion().getContains()) { - if (code.equals(c.getCode()) && (system == null || system.equals(c.getSystem()))) - return true; - if (codeinExpansion(c, system, code)) - return true; - } - return false; - } - - private String describeReference(String reference) { - if (reference == null) - return "null"; - return reference; - } - - private String describeReference(String reference, CanonicalResource target) { - if (reference == null && target == null) - return "null"; - if (reference == null) { - return target.getUrl(); - } - if (target == null) { - return reference; - } - if (reference.equals(target.getUrl())) { - return reference; - } - return reference + "(which actually refers to " + target.getUrl() + ")"; - } - - private String describeTypes(List types) { - CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder(); - for (TypeRefComponent t : types) { - b.append(t.getWorkingCode()); - } - return b.toString(); - } - - protected ElementDefinition findElement(StructureDefinition profile, String name) { - for (ElementDefinition c : profile.getSnapshot().getElement()) { - if (c.getPath().equals(name)) { - return c; - } - } - return null; - } - - public BestPracticeWarningLevel getBestPracticeWarningLevel() { - return bpWarnings; - } - - @Override - public CheckDisplayOption getCheckDisplay() { - return checkDisplay; - } - - private ConceptDefinitionComponent getCodeDefinition(ConceptDefinitionComponent c, String code) { - if (code.equals(c.getCode())) - return c; - for (ConceptDefinitionComponent g : c.getConcept()) { - ConceptDefinitionComponent r = getCodeDefinition(g, code); - if (r != null) - return r; - } - return null; - } - - private ConceptDefinitionComponent getCodeDefinition(CodeSystem cs, String code) { - for (ConceptDefinitionComponent c : cs.getConcept()) { - ConceptDefinitionComponent r = getCodeDefinition(c, code); - if (r != null) - return r; - } - return null; - } - - private IndexedElement getContainedById(Element container, String id) { - List contained = new ArrayList(); - container.getNamedChildren("contained", contained); - for (int i = 0; i < contained.size(); i++) { - Element we = contained.get(i); - if (id.equals(we.getNamedChildValue("id"))) { - return new IndexedElement(i, we, null); - } - } - return null; - } - - public IWorkerContext getContext() { - return context; - } - - private List getCriteriaForDiscriminator(String path, ElementDefinition element, String discriminator, StructureDefinition profile, boolean removeResolve) throws FHIRException { - List elements = new ArrayList(); - if ("value".equals(discriminator) && element.hasFixed()) { - elements.add(element); - return elements; - } - - if (removeResolve) { // if we're doing profile slicing, we don't want to walk into the last resolve.. we need the profile on the source not the target - if (discriminator.equals("resolve()")) { - elements.add(element); - return elements; - } - if (discriminator.endsWith(".resolve()")) - discriminator = discriminator.substring(0, discriminator.length() - 10); - } - - ElementDefinition ed = null; - ExpressionNode expr = fpe.parse(fixExpr(discriminator)); - long t2 = System.nanoTime(); + expr = fpe.parse(fixExpr(discriminator)); + t2 = System.nanoTime(); ed = fpe.evaluateDefinition(expr, profile, element); sdTime = sdTime + (System.nanoTime() - t2); if (ed != null) - elements.add(ed); + elements.add(ed); + } + } + return elements; + } - for (TypeRefComponent type : element.getType()) { - for (CanonicalType p : type.getProfile()) { - String id = p.hasExtension(ToolingExtensions.EXT_PROFILE_ELEMENT) ? p.getExtensionString(ToolingExtensions.EXT_PROFILE_ELEMENT) : null; - StructureDefinition sd = context.fetchResource(StructureDefinition.class, p.getValue()); - if (sd == null) - throw new DefinitionException("Unable to resolve profile " + p); - profile = sd; - if (id == null) - element = sd.getSnapshot().getElementFirstRep(); - else { - element = null; - for (ElementDefinition t : sd.getSnapshot().getElement()) { - if (id.equals(t.getId())) - element = t; - } - if (element == null) - throw new DefinitionException("Unable to resolve element " + id + " in profile " + p); - } - expr = fpe.parse(fixExpr(discriminator)); - t2 = System.nanoTime(); - ed = fpe.evaluateDefinition(expr, profile, element); - sdTime = sdTime + (System.nanoTime() - t2); - if (ed != null) - elements.add(ed); - } + + private Element getExtensionByUrl(List extensions, String urlSimple) { + for (Element e : extensions) { + if (urlSimple.equals(e.getNamedChildValue("url"))) + return e; + } + return null; + } + + public List getExtensionDomains() { + return extensionDomains; + } + + private IndexedElement getFromBundle(Element bundle, String ref, String fullUrl, List 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"),messages.getString("Relative_Reference_appears_inside_Bundle_whose_entry_is_missing_a_fullUrl")); + return null; + + } else if (ref.split("/").length != 2 && ref.split("/").length != 4) { + if (isTransaction) { + rule(errors, IssueType.INVALID, -1, -1, path, isSearchUrl(ref),messages.getString("Relative_URLs_must_be_of_the_format_ResourceNameid_or_a_search_ULR_is_allowed_typeparameters__Encountered_"), ref); + } else { + rule(errors, IssueType.INVALID, -1, -1, path, false,messages.getString("Relative_URLs_must_be_of_the_format_ResourceNameid__Encountered_"), 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] + ":"; } - return elements; - } - - - private Element getExtensionByUrl(List extensions, String urlSimple) { - for (Element e : extensions) { - if (urlSimple.equals(e.getNamedChildValue("url"))) - return e; + } else { + String[] parts; + parts = fullUrl.split("/"); + for (int i = 0; i < parts.length - 2; i++) { + base = base + parts[i] + "/"; } - return null; + } + + 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; } - public List getExtensionDomains() { - return extensionDomains; - } - - private IndexedElement getFromBundle(Element bundle, String ref, String fullUrl, List 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"), "Relative Reference appears inside Bundle whose entry is missing a fullUrl"); - return null; - - } else if (ref.split("/").length != 2 && ref.split("/").length != 4) { - if (isTransaction) { - rule(errors, IssueType.INVALID, -1, -1, path, isSearchUrl(ref), "Relative URLs must be of the format [ResourceName]/[id], or a search ULR is allowed ([type]?parameters. Encountered " + ref + ")"); - } else { - rule(errors, IssueType.INVALID, -1, -1, path, false, "Relative URLs must be of the format [ResourceName]/[id]. Encountered " + ref); - } - return null; - + List entries = new ArrayList(); + 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("fullUrl"))) { + Element r = we.getNamedChild("resource"); + if (version.isEmpty()) { + rule(errors, IssueType.FORBIDDEN, -1, -1, path, match == null,messages.getString("Multiple_matches_in_bundle_for_reference_"), ref); + match = r; + matchIndex = i; } 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] + "/"; - } + try { + if (version.equals(r.getChildren("meta").get(0).getChildValue("versionId"))) { + rule(errors, IssueType.FORBIDDEN, -1, -1, path, match == null,messages.getString("Multiple_matches_in_bundle_for_reference_"), ref); + match = r; + matchIndex = 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; + } catch (Exception e) { + warning(errors, IssueType.REQUIRED, -1, -1, path, r.getChildren("meta").size() == 1 && r.getChildren("meta").get(0).getChildValue("versionId") != null,messages.getString("Entries_matching_fullURL__should_declare_metaversionId_because_there_are_versionspecific_references"), targetUrl); + // If one of these things is null + } } - - List entries = new ArrayList(); - 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("fullUrl"))) { - Element r = we.getNamedChild("resource"); - if (version.isEmpty()) { - rule(errors, IssueType.FORBIDDEN, -1, -1, path, match == null, "Multiple matches in bundle for reference " + 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, "Multiple matches in bundle for reference " + 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, "Entries matching fullURL " + targetUrl + " should declare meta/versionId because there are version-specific references"); - // If one of these things is null - } - } - } - } - - if (match != null && resourceType != null) - rule(errors, IssueType.REQUIRED, -1, -1, path, match.getType().equals(resourceType), "Matching reference for reference " + ref + " has resourceType " + match.getType()); - if (match == null) - warning(errors, IssueType.REQUIRED, -1, -1, path, !ref.startsWith("urn"), "URN reference is not locally contained within the bundle " + 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]*=[^=&]+))*"); - } - } + if (match != null && resourceType != null) + rule(errors, IssueType.REQUIRED, -1, -1, path, match.getType().equals(resourceType),messages.getString("Matching_reference_for_reference__has_resourceType_"), ref, match.getType()); + if (match == null) + warning(errors, IssueType.REQUIRED, -1, -1, path, !ref.startsWith("urn"),messages.getString("URN_reference_is_not_locally_contained_within_the_bundle_"), ref); + return match == null ? null : new IndexedElement(matchIndex, match, entries.get(matchIndex)); + } - private StructureDefinition getProfileForType(String type, List list) { - for (TypeRefComponent tr : list) { - String url = tr.getWorkingCode(); - if (!Utilities.isAbsoluteUrl(url)) - url = "http://hl7.org/fhir/StructureDefinition/" + url; - long t = System.nanoTime(); - StructureDefinition sd = context.fetchResource(StructureDefinition.class, url); - sdTime = sdTime + (System.nanoTime() - t); - if (sd != null && (sd.getType().equals(type) || sd.getUrl().equals(type)) && sd.hasSnapshot()) - return sd; - } - return null; + 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 Element getValueForDiscriminator(Object appContext, List errors, Element element, String discriminator, ElementDefinition criteria, NodeStack stack) throws FHIRException, IOException { - String p = stack.getLiteralPath() + "." + element.getName(); - Element focus = element; - String[] dlist = discriminator.split("\\."); - for (String d : dlist) { - if (focus.fhirType().equals("Reference") && d.equals("reference")) { - String url = focus.getChildValue("reference"); - if (Utilities.noString(url)) - throw new FHIRException("No reference resolving discriminator " + discriminator + " from " + element.getProperty().getName()); - // Note that we use the passed in stack here. This might be a problem if the discriminator is deep enough? - Element target = resolve(appContext, url, stack, errors, p); - if (target == null) - throw new FHIRException("Unable to find resource " + url + " at " + d + " resolving discriminator " + discriminator + " from " + element.getProperty().getName()); - focus = target; - } else if (d.equals("value") && focus.isPrimitive()) { - return focus; - } else { - List children = focus.getChildren(d); - if (children.isEmpty()) - throw new FHIRException("Unable to find " + d + " resolving discriminator " + discriminator + " from " + element.getProperty().getName()); - if (children.size() > 1) - throw new FHIRException("Found " + Integer.toString(children.size()) + " items for " + d + " resolving discriminator " + discriminator + " from " + element.getProperty().getName()); - focus = children.get(0); - p = p + "." + d; - } - } + private StructureDefinition getProfileForType(String type, List list) { + for (TypeRefComponent tr : list) { + String url = tr.getWorkingCode(); + if (!Utilities.isAbsoluteUrl(url)) + url = "http://hl7.org/fhir/StructureDefinition/" + url; + long t = System.nanoTime(); + StructureDefinition sd = context.fetchResource(StructureDefinition.class, url); + sdTime = sdTime + (System.nanoTime() - t); + if (sd != null && (sd.getType().equals(type) || sd.getUrl().equals(type)) && sd.hasSnapshot()) + return sd; + } + return null; + } + + private Element getValueForDiscriminator(Object appContext, List errors, Element element, String discriminator, ElementDefinition criteria, NodeStack stack) throws FHIRException, IOException { + String p = stack.getLiteralPath() + "." + element.getName(); + Element focus = element; + String[] dlist = discriminator.split("\\."); + for (String d : dlist) { + if (focus.fhirType().equals("Reference") && d.equals("reference")) { + String url = focus.getChildValue("reference"); + if (Utilities.noString(url)) + throw new FHIRException("No reference resolving discriminator " + discriminator + " from " + element.getProperty().getName()); + // Note that we use the passed in stack here. This might be a problem if the discriminator is deep enough? + Element target = resolve(appContext, url, stack, errors, p); + if (target == null) + throw new FHIRException("Unable to find resource " + url + " at " + d + " resolving discriminator " + discriminator + " from " + element.getProperty().getName()); + focus = target; + } else if (d.equals("value") && focus.isPrimitive()) { return focus; + } else { + List children = focus.getChildren(d); + if (children.isEmpty()) + throw new FHIRException("Unable to find " + d + " resolving discriminator " + discriminator + " from " + element.getProperty().getName()); + if (children.size() > 1) + throw new FHIRException("Found " + Integer.toString(children.size()) + " items for " + d + " resolving discriminator " + discriminator + " from " + element.getProperty().getName()); + focus = children.get(0); + p = p + "." + d; + } } + return focus; + } - private CodeSystem getCodeSystem(String system) { - long t = System.nanoTime(); - try { - return context.fetchCodeSystem(system); - } finally { - txTime = txTime + (System.nanoTime() - t); + private CodeSystem getCodeSystem(String system) { + long t = System.nanoTime(); + try { + return context.fetchCodeSystem(system); + } finally { + txTime = txTime + (System.nanoTime() - t); + } + } + + private boolean hasTime(String fmt) { + return fmt.contains("T"); + } + + private boolean hasTimeZone(String fmt) { + return fmt.length() > 10 && (fmt.substring(10).contains("-") || fmt.substring(10).contains("+") || fmt.substring(10).contains("Z")); + } + + private boolean isAbsolute(String uri) { + return Utilities.noString(uri) || uri.startsWith("http:") || uri.startsWith("https:") || uri.startsWith("urn:uuid:") || uri.startsWith("urn:oid:") || uri.startsWith("urn:ietf:") + || uri.startsWith("urn:iso:") || uri.startsWith("urn:iso-astm:") || isValidFHIRUrn(uri); + } + + private boolean isValidFHIRUrn(String uri) { + return (uri.equals("urn:x-fhir:uk:id:nhs-number")) || uri.startsWith("urn:"); // Anyone can invent a URN, so why should we complain? + } + + public boolean isAnyExtensionsAllowed() { + return anyExtensionsAllowed; + } + + public boolean isErrorForUnknownProfiles() { + return errorForUnknownProfiles; + } + + public void setErrorForUnknownProfiles(boolean errorForUnknownProfiles) { + this.errorForUnknownProfiles = errorForUnknownProfiles; + } + + private boolean isParametersEntry(String path) { + String[] parts = path.split("\\."); + return parts.length > 2 && parts[parts.length - 1].equals("resource") && (pathEntryHasName(parts[parts.length - 2], "parameter") || pathEntryHasName(parts[parts.length - 2], "part")); + } + + private boolean isBundleEntry(String path) { + String[] parts = path.split("\\."); + return parts.length > 2 && parts[parts.length - 1].equals("resource") && pathEntryHasName(parts[parts.length - 2], "entry"); + } + + private boolean isBundleOutcome(String path) { + String[] parts = path.split("\\."); + return parts.length > 2 && parts[parts.length - 1].equals("outcome") && pathEntryHasName(parts[parts.length - 2], "response"); + } + + + private static boolean pathEntryHasName(String thePathEntry, String theName) { + if (thePathEntry.equals(theName)) { + return true; + } + if (thePathEntry.length() >= theName.length() + 3) { + if (thePathEntry.startsWith(theName)) { + if (thePathEntry.charAt(theName.length()) == '[') { + return true; } + } } + return false; + } - private boolean hasTime(String fmt) { - return fmt.contains("T"); - } + public boolean isPrimitiveType(String code) { + StructureDefinition sd = context.fetchTypeDefinition(code); + return sd != null && sd.getKind() == StructureDefinitionKind.PRIMITIVETYPE; + } - private boolean hasTimeZone(String fmt) { - return fmt.length() > 10 && (fmt.substring(10).contains("-") || fmt.substring(10).contains("+") || fmt.substring(10).contains("Z")); - } + private String getErrorMessage(String message) { + return message != null ? " (error message = " + message + ")" : ""; + } - private boolean isAbsolute(String uri) { - return Utilities.noString(uri) || uri.startsWith("http:") || uri.startsWith("https:") || uri.startsWith("urn:uuid:") || uri.startsWith("urn:oid:") || uri.startsWith("urn:ietf:") - || uri.startsWith("urn:iso:") || uri.startsWith("urn:iso-astm:") || isValidFHIRUrn(uri); - } + public boolean isSuppressLoincSnomedMessages() { + return suppressLoincSnomedMessages; + } - private boolean isValidFHIRUrn(String uri) { - return (uri.equals("urn:x-fhir:uk:id:nhs-number")) || uri.startsWith("urn:"); // Anyone can invent a URN, so why should we complain? - } + private boolean nameMatches(String name, String tail) { + if (tail.endsWith("[x]")) + return name.startsWith(tail.substring(0, tail.length() - 3)); + else + return (name.equals(tail)); + } - public boolean isAnyExtensionsAllowed() { - return anyExtensionsAllowed; - } - - public boolean isErrorForUnknownProfiles() { - return errorForUnknownProfiles; - } - - public void setErrorForUnknownProfiles(boolean errorForUnknownProfiles) { - this.errorForUnknownProfiles = errorForUnknownProfiles; - } - - private boolean isParametersEntry(String path) { - String[] parts = path.split("\\."); - return parts.length > 2 && parts[parts.length - 1].equals("resource") && (pathEntryHasName(parts[parts.length - 2], "parameter") || pathEntryHasName(parts[parts.length - 2], "part")); - } - - private boolean isBundleEntry(String path) { - String[] parts = path.split("\\."); - return parts.length > 2 && parts[parts.length - 1].equals("resource") && pathEntryHasName(parts[parts.length - 2], "entry"); - } - - private boolean isBundleOutcome(String path) { - String[] parts = path.split("\\."); - return parts.length > 2 && parts[parts.length - 1].equals("outcome") && pathEntryHasName(parts[parts.length - 2], "response"); - } - - - private static boolean pathEntryHasName(String thePathEntry, String theName) { - if (thePathEntry.equals(theName)) { - return true; - } - if (thePathEntry.length() >= theName.length() + 3) { - if (thePathEntry.startsWith(theName)) { - if (thePathEntry.charAt(theName.length()) == '[') { - return true; - } - } - } - return false; - } - - public boolean isPrimitiveType(String code) { - StructureDefinition sd = context.fetchTypeDefinition(code); - return sd != null && sd.getKind() == StructureDefinitionKind.PRIMITIVETYPE; - } - - private String getErrorMessage(String message) { - return message != null ? " (error message = " + message + ")" : ""; - } - - public boolean isSuppressLoincSnomedMessages() { - return suppressLoincSnomedMessages; - } - - private boolean nameMatches(String name, String tail) { - if (tail.endsWith("[x]")) - return name.startsWith(tail.substring(0, tail.length() - 3)); + private boolean passesCodeWhitespaceRules(String v) { + if (!v.trim().equals(v)) + return false; + boolean lastWasSpace = true; + for (char c : v.toCharArray()) { + if (c == ' ') { + if (lastWasSpace) + return false; else - return (name.equals(tail)); + lastWasSpace = true; + } else if (Character.isWhitespace(c)) + return false; + else + lastWasSpace = false; } + return true; + } - private boolean passesCodeWhitespaceRules(String v) { - if (!v.trim().equals(v)) - return false; - boolean lastWasSpace = true; - for (char c : v.toCharArray()) { - if (c == ' ') { - if (lastWasSpace) - return false; - else - lastWasSpace = true; - } else if (Character.isWhitespace(c)) - return false; - else - lastWasSpace = false; + private ResolvedReference localResolve(String ref, NodeStack stack, List errors, String path, Element hostContext, Element source) { + if (ref.startsWith("#")) { + // work back through the parent list. + // really, there should only be one level for this (contained resources cannot contain + // contained resources), but we'll leave that to some other code to worry about + while (stack != null && stack.getElement() != null) { + if (stack.getElement().getProperty().isResource()) { + // ok, we'll try to find the contained reference + IndexedElement res = getContainedById(stack.getElement(), ref.substring(1)); + if (res != null) { + ResolvedReference rr = new ResolvedReference(); + rr.setResource(stack.getElement()); + rr.setFocus(res.getMatch()); + rr.setExternal(false); + rr.setStack(stack.push(res.getMatch(), res.getIndex(), res.getMatch().getProperty().getDefinition(), res.getMatch().getProperty().getDefinition())); + return rr; + } } - return true; - } - - private ResolvedReference localResolve(String ref, NodeStack stack, List errors, String path, Element hostContext, Element source) { - if (ref.startsWith("#")) { - // work back through the parent list. - // really, there should only be one level for this (contained resources cannot contain - // contained resources), but we'll leave that to some other code to worry about - while (stack != null && stack.getElement() != null) { - if (stack.getElement().getProperty().isResource()) { - // ok, we'll try to find the contained reference - IndexedElement res = getContainedById(stack.getElement(), ref.substring(1)); - if (res != null) { - ResolvedReference rr = new ResolvedReference(); - rr.setResource(stack.getElement()); - rr.setFocus(res.getMatch()); - rr.setExternal(false); - rr.setStack(stack.push(res.getMatch(), res.getIndex(), res.getMatch().getProperty().getDefinition(), res.getMatch().getProperty().getDefinition())); - return rr; - } - } - if (stack.getElement().getSpecial() == SpecialElement.BUNDLE_ENTRY) { - return null; // we don't try to resolve contained references across this boundary - } - stack = stack.parent; - } + if (stack.getElement().getSpecial() == SpecialElement.BUNDLE_ENTRY) { + return null; // we don't try to resolve contained references across this boundary + } + stack = stack.parent; + } + return null; + } else { + // work back through the parent list - if any of them are bundles, try to resolve + // the resource in the bundle + String fullUrl = null; // we're going to try to work this out as we go up + while (stack != null && stack.getElement() != null) { + if (stack.getElement().getSpecial() == SpecialElement.BUNDLE_ENTRY && fullUrl == null && stack.parent != null && stack.parent.getElement().getName().equals("entry")) { + String type = stack.parent.parent.element.getChildValue("type"); + fullUrl = stack.parent.getElement().getChildValue("fullUrl"); // we don't try to resolve contained references across this boundary + if (fullUrl == null) + rule(errors, IssueType.REQUIRED, stack.parent.getElement().line(), stack.parent.getElement().col(), stack.parent.getLiteralPath(), Utilities.existsInList(type, "batch-response", "transaction-response") || fullUrl != null,messages.getString("Bundle_entry_missing_fullUrl")); + } + if ("Bundle".equals(stack.getElement().getType())) { + String type = stack.getElement().getChildValue("type"); + IndexedElement res = getFromBundle(stack.getElement(), ref, fullUrl, errors, path, type, "transaction".equals(type)); + if (res == null) { return null; + } else { + ResolvedReference rr = new ResolvedReference(); + rr.setResource(res.getMatch()); + rr.setFocus(res.getMatch()); + rr.setExternal(false); + rr.setStack(stack.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())); + return rr; + } + } + stack = stack.parent; + } + // we can get here if we got called via FHIRPath conformsTo which breaks the stack continuity. + if (hostContext != null && "Bundle".equals(hostContext.fhirType())) { + String type = hostContext.getChildValue("type"); + Element entry = getEntryForSource(hostContext, source); + fullUrl = entry.getChildValue("fullUrl"); + IndexedElement res = getFromBundle(hostContext, ref, fullUrl, errors, path, type, "transaction".equals(type)); + if (res == null) { + return null; } else { - // work back through the parent list - if any of them are bundles, try to resolve - // the resource in the bundle - String fullUrl = null; // we're going to try to work this out as we go up - while (stack != null && stack.getElement() != null) { - if (stack.getElement().getSpecial() == SpecialElement.BUNDLE_ENTRY && fullUrl == null && stack.parent != null && stack.parent.getElement().getName().equals("entry")) { - String type = stack.parent.parent.element.getChildValue("type"); - fullUrl = stack.parent.getElement().getChildValue("fullUrl"); // we don't try to resolve contained references across this boundary - if (fullUrl == null) - rule(errors, IssueType.REQUIRED, stack.parent.getElement().line(), stack.parent.getElement().col(), stack.parent.getLiteralPath(), - Utilities.existsInList(type, "batch-response", "transaction-response") || fullUrl != null, "Bundle entry missing fullUrl"); - } - if ("Bundle".equals(stack.getElement().getType())) { - String type = stack.getElement().getChildValue("type"); - IndexedElement res = getFromBundle(stack.getElement(), ref, fullUrl, errors, path, type, "transaction".equals(type)); - if (res == null) { - return null; - } else { - ResolvedReference rr = new ResolvedReference(); - rr.setResource(res.getMatch()); - rr.setFocus(res.getMatch()); - rr.setExternal(false); - rr.setStack(stack.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())); - return rr; - } - } - stack = stack.parent; - } - // we can get here if we got called via FHIRPath conformsTo which breaks the stack continuity. - if (hostContext != null && "Bundle".equals(hostContext.fhirType())) { - String type = hostContext.getChildValue("type"); - Element entry = getEntryForSource(hostContext, source); - fullUrl = entry.getChildValue("fullUrl"); - IndexedElement res = getFromBundle(hostContext, ref, fullUrl, errors, path, type, "transaction".equals(type)); - if (res == null) { - return null; - } else { - ResolvedReference rr = new ResolvedReference(); - rr.setResource(res.getMatch()); - rr.setFocus(res.getMatch()); - rr.setExternal(false); - rr.setStack(new NodeStack(hostContext).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())); - return rr; - } - } + ResolvedReference rr = new ResolvedReference(); + rr.setResource(res.getMatch()); + rr.setFocus(res.getMatch()); + rr.setExternal(false); + rr.setStack(new NodeStack(hostContext).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())); + return rr; + } + } + } + return null; + } + + private Element getEntryForSource(Element bundle, Element element) { + List entries = new ArrayList(); + bundle.getNamedChildren("entry", entries); + for (Element entry : entries) { + if (entry.hasDescendant(element)) { + return entry; + } + } + return null; + } + + private ResolvedReference makeExternalRef(Element external, String path) { + ResolvedReference res = new ResolvedReference(); + res.setResource(external); + res.setFocus(external); + res.setExternal(true); + res.setStack(new NodeStack(external, path)); + return res; + } + + + private Element resolve(Object appContext, String ref, NodeStack stack, List errors, String path) throws IOException, FHIRException { + Element local = localResolve(ref, stack, errors, path, null, null).getFocus(); + if (local != null) + return local; + if (fetcher == null) + return null; + if (fetchCache.containsKey(ref)) { + return fetchCache.get(ref); + } else { + Element res = fetcher.fetch(appContext, ref); + fetchCache.put(ref, res); + return res; + } + } + + private ValueSet resolveBindingReference(DomainResource ctxt, String reference, String uri) { + if (reference != null) { + if (reference.startsWith("#")) { + for (Resource c : ctxt.getContained()) { + if (c.getId().equals(reference.substring(1)) && (c instanceof ValueSet)) + return (ValueSet) c; } return null; - } + } else { + long t = System.nanoTime(); + ValueSet fr = context.fetchResource(ValueSet.class, reference); + if (fr == null) { + if (!Utilities.isAbsoluteUrl(reference)) { + reference = resolve(uri, reference); + fr = context.fetchResource(ValueSet.class, reference); + } + } + if (fr == null) + fr = ValueSetUtilities.generateImplicitValueSet(reference); + txTime = txTime + (System.nanoTime() - t); + return fr; + } + } else + return null; + } - private Element getEntryForSource(Element bundle, Element element) { - List entries = new ArrayList(); - bundle.getNamedChildren("entry", entries); + private String resolve(String uri, String ref) { + if (isBlank(uri)) { + return ref; + } + String[] up = uri.split("\\/"); + String[] rp = ref.split("\\/"); + if (context.getResourceNames().contains(up[up.length - 2]) && context.getResourceNames().contains(rp[0])) { + StringBuilder b = new StringBuilder(); + for (int i = 0; i < up.length - 2; i++) { + b.append(up[i]); + b.append("/"); + } + b.append(ref); + return b.toString(); + } else + return ref; + } + + private Element resolveInBundle(List 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("fullUrl"); + 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) { - if (entry.hasDescendant(element)) { + String fu = entry.getNamedChildValue("fullUrl"); + 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; + } + return null; } + } - private ResolvedReference makeExternalRef(Element external, String path) { - ResolvedReference res = new ResolvedReference(); - res.setResource(external); - res.setFocus(external); - res.setExternal(true); - res.setStack(new NodeStack(external, path)); - return res; + private ElementDefinition resolveNameReference(StructureDefinitionSnapshotComponent snapshot, String contentReference) { + for (ElementDefinition ed : snapshot.getElement()) + if (contentReference.equals("#" + ed.getId())) + return ed; + return null; + } + + private StructureDefinition resolveProfile(StructureDefinition profile, String pr) { + if (pr.startsWith("#")) { + for (Resource r : profile.getContained()) { + if (r.getId().equals(pr.substring(1)) && r instanceof StructureDefinition) + return (StructureDefinition) r; + } + return null; + } else { + long t = System.nanoTime(); + StructureDefinition fr = context.fetchResource(StructureDefinition.class, pr); + sdTime = sdTime + (System.nanoTime() - t); + return fr; } + } - - private Element resolve(Object appContext, String ref, NodeStack stack, List errors, String path) throws IOException, FHIRException { - Element local = localResolve(ref, stack, errors, path, null, null).getFocus(); - if (local != null) - return local; - if (fetcher == null) - return null; - if (fetchCache.containsKey(ref)) { - return fetchCache.get(ref); - } else { - Element res = fetcher.fetch(appContext, ref); - fetchCache.put(ref, res); - return res; - } + private ElementDefinition resolveType(String type, List list) { + for (TypeRefComponent tr : list) { + String url = tr.getWorkingCode(); + if (!Utilities.isAbsoluteUrl(url)) + url = "http://hl7.org/fhir/StructureDefinition/" + url; + long t = System.nanoTime(); + StructureDefinition sd = context.fetchResource(StructureDefinition.class, url); + sdTime = sdTime + (System.nanoTime() - t); + if (sd != null && (sd.getType().equals(type) || sd.getUrl().equals(type)) && sd.hasSnapshot()) + return sd.getSnapshot().getElement().get(0); } + return null; + } - private ValueSet resolveBindingReference(DomainResource ctxt, String reference, String uri) { - if (reference != null) { - if (reference.startsWith("#")) { - for (Resource c : ctxt.getContained()) { - if (c.getId().equals(reference.substring(1)) && (c instanceof ValueSet)) - return (ValueSet) c; - } - return null; - } else { - long t = System.nanoTime(); - ValueSet fr = context.fetchResource(ValueSet.class, reference); - if (fr == null) { - if (!Utilities.isAbsoluteUrl(reference)) { - reference = resolve(uri, reference); - fr = context.fetchResource(ValueSet.class, reference); - } - } - if (fr == null) - fr = ValueSetUtilities.generateImplicitValueSet(reference); - txTime = txTime + (System.nanoTime() - t); - return fr; - } - } else - return null; - } + public void setAnyExtensionsAllowed(boolean anyExtensionsAllowed) { + this.anyExtensionsAllowed = anyExtensionsAllowed; + } - private String resolve(String uri, String ref) { - if (isBlank(uri)) { - return ref; - } - String[] up = uri.split("\\/"); - String[] rp = ref.split("\\/"); - if (context.getResourceNames().contains(up[up.length - 2]) && context.getResourceNames().contains(rp[0])) { - StringBuilder b = new StringBuilder(); - for (int i = 0; i < up.length - 2; i++) { - b.append(up[i]); - b.append("/"); - } - b.append(ref); - return b.toString(); - } else - return ref; - } + public IResourceValidator setBestPracticeWarningLevel(BestPracticeWarningLevel value) { + bpWarnings = value; + return this; + } - private Element resolveInBundle(List 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("fullUrl"); - 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("fullUrl"); - 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; - } - } + @Override + public void setCheckDisplay(CheckDisplayOption checkDisplay) { + this.checkDisplay = checkDisplay; + } - private ElementDefinition resolveNameReference(StructureDefinitionSnapshotComponent snapshot, String contentReference) { - for (ElementDefinition ed : snapshot.getElement()) - if (contentReference.equals("#" + ed.getId())) - return ed; - return null; - } + public void setSuppressLoincSnomedMessages(boolean suppressLoincSnomedMessages) { + this.suppressLoincSnomedMessages = suppressLoincSnomedMessages; + } - private StructureDefinition resolveProfile(StructureDefinition profile, String pr) { - if (pr.startsWith("#")) { - for (Resource r : profile.getContained()) { - if (r.getId().equals(pr.substring(1)) && r instanceof StructureDefinition) - return (StructureDefinition) r; - } - return null; - } else { - long t = System.nanoTime(); - StructureDefinition fr = context.fetchResource(StructureDefinition.class, pr); - sdTime = sdTime + (System.nanoTime() - t); - return fr; - } - } + public IdStatus getResourceIdRule() { + return resourceIdRule; + } - private ElementDefinition resolveType(String type, List list) { - for (TypeRefComponent tr : list) { - String url = tr.getWorkingCode(); - if (!Utilities.isAbsoluteUrl(url)) - url = "http://hl7.org/fhir/StructureDefinition/" + url; - long t = System.nanoTime(); - StructureDefinition sd = context.fetchResource(StructureDefinition.class, url); - sdTime = sdTime + (System.nanoTime() - t); - if (sd != null && (sd.getType().equals(type) || sd.getUrl().equals(type)) && sd.hasSnapshot()) - return sd.getSnapshot().getElement().get(0); - } - return null; - } - - public void setAnyExtensionsAllowed(boolean anyExtensionsAllowed) { - this.anyExtensionsAllowed = anyExtensionsAllowed; - } - - public IResourceValidator setBestPracticeWarningLevel(BestPracticeWarningLevel value) { - bpWarnings = value; - return this; - } - - @Override - public void setCheckDisplay(CheckDisplayOption checkDisplay) { - this.checkDisplay = checkDisplay; - } - - public void setSuppressLoincSnomedMessages(boolean suppressLoincSnomedMessages) { - this.suppressLoincSnomedMessages = suppressLoincSnomedMessages; - } - - public IdStatus getResourceIdRule() { - return resourceIdRule; - } - - public void setResourceIdRule(IdStatus resourceIdRule) { - this.resourceIdRule = resourceIdRule; - } + public void setResourceIdRule(IdStatus resourceIdRule) { + this.resourceIdRule = resourceIdRule; + } - public boolean isAllowXsiLocation() { - return allowXsiLocation; - } + public boolean isAllowXsiLocation() { + return allowXsiLocation; + } - public void setAllowXsiLocation(boolean allowXsiLocation) { - this.allowXsiLocation = allowXsiLocation; - } + public void setAllowXsiLocation(boolean allowXsiLocation) { + this.allowXsiLocation = allowXsiLocation; + } - /** - * @param element - the candidate that might be in the slice - * @param path - for reporting any errors. the XPath for the element - * @param slicer - the definition of how slicing is determined - * @param ed - the slice for which to test membership - * @param errors - * @param stack - * @return - * @throws DefinitionException - * @throws DefinitionException - * @throws IOException - * @throws FHIRException - */ - private boolean sliceMatches(ValidatorHostContext hostContext, Element element, String path, ElementDefinition slicer, ElementDefinition ed, StructureDefinition profile, List errors, List sliceInfo, NodeStack stack) throws DefinitionException, FHIRException { - if (!slicer.getSlicing().hasDiscriminator()) - return false; // cannot validate in this case + /** + * @param element - the candidate that might be in the slice + * @param path - for reporting any errors. the XPath for the element + * @param slicer - the definition of how slicing is determined + * @param ed - the slice for which to test membership + * @param errors + * @param stack + * @return + * @throws DefinitionException + * @throws DefinitionException + * @throws IOException + * @throws FHIRException + */ + private boolean sliceMatches(ValidatorHostContext hostContext, Element element, String path, ElementDefinition slicer, ElementDefinition ed, StructureDefinition profile, List errors, List sliceInfo, NodeStack stack) throws DefinitionException, FHIRException { + if (!slicer.getSlicing().hasDiscriminator()) + return false; // cannot validate in this case - ExpressionNode n = (ExpressionNode) ed.getUserData("slice.expression.cache"); - if (n == null) { - long t = System.nanoTime(); - // GG: this approach is flawed because it treats discriminators individually rather than collectively - StringBuilder expression = new StringBuilder("true"); - boolean anyFound = false; - Set discriminators = new HashSet<>(); - for (ElementDefinitionSlicingDiscriminatorComponent s : slicer.getSlicing().getDiscriminator()) { - String discriminator = s.getPath(); - discriminators.add(discriminator); + ExpressionNode n = (ExpressionNode) ed.getUserData("slice.expression.cache"); + if (n == null) { + long t = System.nanoTime(); + // GG: this approach is flawed because it treats discriminators individually rather than collectively + StringBuilder expression = new StringBuilder("true"); + boolean anyFound = false; + Set discriminators = new HashSet<>(); + for (ElementDefinitionSlicingDiscriminatorComponent s : slicer.getSlicing().getDiscriminator()) { + String discriminator = s.getPath(); + discriminators.add(discriminator); - List criteriaElements = getCriteriaForDiscriminator(path, ed, discriminator, profile, s.getType() == DiscriminatorType.PROFILE); - boolean found = false; - for (ElementDefinition criteriaElement : criteriaElements) { - found = true; - if (s.getType() == DiscriminatorType.TYPE) { - String type = null; - if (!criteriaElement.getPath().contains("[") && discriminator.contains("[")) { - discriminator = discriminator.substring(0, discriminator.indexOf('[')); - String lastNode = tail(discriminator); - type = tail(criteriaElement.getPath()).substring(lastNode.length()); - type = type.substring(0, 1).toLowerCase() + type.substring(1); - } else if (!criteriaElement.hasType() || criteriaElement.getType().size() == 1) { - if (discriminator.contains("[")) - discriminator = discriminator.substring(0, discriminator.indexOf('[')); - type = criteriaElement.getType().get(0).getWorkingCode(); - } else if (criteriaElement.getType().size() > 1) { - throw new DefinitionException("Discriminator (" + discriminator + ") is based on type, but slice " + ed.getId() + " in " + profile.getUrl() + " has multiple types: " + criteriaElement.typeSummary()); - } else - throw new DefinitionException("Discriminator (" + discriminator + ") is based on type, but slice " + ed.getId() + " in " + profile.getUrl() + " has no types"); - if (discriminator.isEmpty()) - expression.append(" and $this is " + type); - else - expression.append(" and " + discriminator + " is " + type); - } else if (s.getType() == DiscriminatorType.PROFILE) { - if (criteriaElement.getType().size() == 0) { - throw new DefinitionException("Profile based discriminators must have a type (" + criteriaElement.getId() + " in profile " + profile.getUrl() + ")"); - } - if (criteriaElement.getType().size() != 1) { - throw new DefinitionException("Profile based discriminators must have only one type (" + criteriaElement.getId() + " in profile " + profile.getUrl() + ")"); - } - List list = discriminator.endsWith(".resolve()") || discriminator.equals("resolve()") ? criteriaElement.getType().get(0).getTargetProfile() : criteriaElement.getType().get(0).getProfile(); - if (list.size() == 0) { - throw new DefinitionException("Profile based discriminators must have a type with a profile (" + criteriaElement.getId() + " in profile " + profile.getUrl() + ")"); - } else if (list.size() > 1) { - CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder(" or "); - for (CanonicalType c : list) { - b.append(discriminator + ".conformsTo('" + c.getValue() + "')"); - } - expression.append(" and (" + b + ")"); - } else { - expression.append(" and " + discriminator + ".conformsTo('" + list.get(0).getValue() + "')"); - } - } else if (s.getType() == DiscriminatorType.EXISTS) { - if (criteriaElement.hasMin() && criteriaElement.getMin() >= 1) - expression.append(" and (" + discriminator + ".exists())"); - else if (criteriaElement.hasMax() && criteriaElement.getMax().equals("0")) - expression.append(" and (" + discriminator + ".exists().not())"); - else - throw new FHIRException("Discriminator (" + discriminator + ") is based on element existence, but slice " + ed.getId() + " neither sets min>=1 or max=0"); - } else if (criteriaElement.hasFixed()) { - buildFixedExpression(ed, expression, discriminator, criteriaElement); - } else if (criteriaElement.hasPattern()) { - buildPattternExpression(ed, expression, discriminator, criteriaElement); - } else if (criteriaElement.hasBinding() && criteriaElement.getBinding().hasStrength() && criteriaElement.getBinding().getStrength().equals(BindingStrength.REQUIRED) && criteriaElement.getBinding().hasValueSet()) { - expression.append(" and (" + discriminator + " memberOf '" + criteriaElement.getBinding().getValueSet() + "')"); - } else { - found = false; - } - if (found) - break; - } - if (found) - anyFound = true; - } - if (!anyFound) { - if (slicer.getSlicing().getDiscriminator().size() > 1) - throw new DefinitionException("Could not match any discriminators (" + discriminators + ") for slice " + ed.getId() + " in profile " + profile.getUrl() + " - None of the discriminator " + discriminators + " have fixed value, binding or existence assertions"); - else - throw new DefinitionException("Could not match discriminator (" + discriminators + ") for slice " + ed.getId() + " in profile " + profile.getUrl() + " - the discriminator " + discriminators + " does not have fixed value, binding or existence assertions"); - } - - try { - n = fpe.parse(fixExpr(expression.toString())); - } catch (FHIRLexerException e) { - throw new FHIRException("Problem processing expression " + expression + " in profile " + profile.getUrl() + " path " + path + ": " + e.getMessage()); - } - fpeTime = fpeTime + (System.nanoTime() - t); - ed.setUserData("slice.expression.cache", n); - } - - ValidatorHostContext shc = hostContext.forSlicing(); - boolean pass = evaluateSlicingExpression(shc, element, path, profile, n); - if (!pass) { - slicingHint(sliceInfo, IssueType.STRUCTURE, element.line(), element.col(), path, false, "Does not match slice'" + ed.getSliceName(), "discriminator = " + Utilities.escapeXml(n.toString())); - for (String url : shc.getSliceRecords().keySet()) { - slicingHint(sliceInfo, IssueType.STRUCTURE, element.line(), element.col(), path, false, "Details for " + stack.getLiteralPath() + " against profile " + url, - "Profile " + url + " does not match for " + stack.getLiteralPath() + " because of the following profile issues: " + errorSummaryForSlicingAsHtml(shc.getSliceRecords().get(url))); - } - } - return pass; - } - - public boolean evaluateSlicingExpression(ValidatorHostContext hostContext, Element element, String path, StructureDefinition profile, ExpressionNode n) throws FHIRException { - String msg; - boolean ok; - try { - long t = System.nanoTime(); - ok = fpe.evaluateToBoolean(hostContext.forProfile(profile), hostContext.getResource(), hostContext.getRootResource(), element, n); - fpeTime = fpeTime + (System.nanoTime() - t); - msg = fpe.forLog(); - } catch (Exception ex) { - ex.printStackTrace(); - throw new FHIRException("Problem evaluating slicing expression for element in profile " + profile.getUrl() + " path " + path + " (fhirPath = " + n + "): " + ex.getMessage()); - } - return ok; - } - - private void buildPattternExpression(ElementDefinition ed, StringBuilder expression, String discriminator, ElementDefinition criteriaElement) throws DefinitionException { - DataType pattern = criteriaElement.getPattern(); - if (pattern instanceof CodeableConcept) { - CodeableConcept cc = (CodeableConcept) pattern; - expression.append(" and "); - buildCodeableConceptExpression(ed, expression, discriminator, cc); - } else if (pattern instanceof Identifier) { - Identifier ii = (Identifier) pattern; - expression.append(" and "); - buildIdentifierExpression(ed, expression, discriminator, ii); - } else - throw new DefinitionException("Unsupported fixed pattern type for discriminator(" + discriminator + ") for slice " + ed.getId() + ": " + pattern.getClass().getName()); - } - - private void buildIdentifierExpression(ElementDefinition ed, StringBuilder expression, String discriminator, Identifier ii) - throws DefinitionException { - if (ii.hasExtension()) - throw new DefinitionException("Unsupported Identifier pattern - extensions are not allowed - for discriminator(" + discriminator + ") for slice " + ed.getId()); - boolean first = true; - expression.append(discriminator + ".where("); - if (ii.hasSystem()) { - first = false; - expression.append("system = '" + ii.getSystem() + "'"); - } - if (ii.hasValue()) { - if (first) - first = false; - else - expression.append(" and "); - expression.append("value = '" + ii.getValue() + "'"); - } - if (ii.hasUse()) { - if (first) - first = false; - else - expression.append(" and "); - expression.append("use = '" + ii.getUse() + "'"); - } - if (ii.hasType()) { - if (first) - first = false; - else - expression.append(" and "); - buildCodeableConceptExpression(ed, expression, "type", ii.getType()); - } - expression.append(").exists()"); - } - - private void buildCodeableConceptExpression(ElementDefinition ed, StringBuilder expression, String discriminator, CodeableConcept cc) - throws DefinitionException { - if (cc.hasText()) - throw new DefinitionException("Unsupported CodeableConcept pattern - using text - for discriminator(" + discriminator + ") for slice " + ed.getId()); - if (!cc.hasCoding()) - throw new DefinitionException("Unsupported CodeableConcept pattern - must have at least one coding - for discriminator(" + discriminator + ") for slice " + ed.getId()); - if (cc.hasExtension()) - throw new DefinitionException("Unsupported CodeableConcept pattern - extensions are not allowed - for discriminator(" + discriminator + ") for slice " + ed.getId()); - boolean firstCoding = true; - for (Coding c : cc.getCoding()) { - if (c.hasExtension()) - throw new DefinitionException("Unsupported CodeableConcept pattern - extensions are not allowed - for discriminator(" + discriminator + ") for slice " + ed.getId()); - if (firstCoding) firstCoding = false; - else expression.append(" and "); - expression.append(discriminator + ".coding.where("); - boolean first = true; - if (c.hasSystem()) { - first = false; - expression.append("system = '" + c.getSystem() + "'"); - } - if (c.hasVersion()) { - if (first) first = false; - else expression.append(" and "); - expression.append("version = '" + c.getVersion() + "'"); - } - if (c.hasCode()) { - if (first) first = false; - else expression.append(" and "); - expression.append("code = '" + c.getCode() + "'"); - } - if (c.hasDisplay()) { - if (first) first = false; - else expression.append(" and "); - expression.append("display = '" + c.getDisplay() + "'"); - } - expression.append(").exists()"); - } - } - - private void buildFixedExpression(ElementDefinition ed, StringBuilder expression, String discriminator, ElementDefinition criteriaElement) throws DefinitionException { - DataType fixed = criteriaElement.getFixed(); - if (fixed instanceof CodeableConcept) { - CodeableConcept cc = (CodeableConcept) fixed; - expression.append(" and "); - buildCodeableConceptExpression(ed, expression, discriminator, cc); - } else if (fixed instanceof Identifier) { - Identifier ii = (Identifier) fixed; - expression.append(" and "); - buildIdentifierExpression(ed, expression, discriminator, ii); - } else { - expression.append(" and ("); - if (fixed instanceof StringType) { - Gson gson = new Gson(); - String json = gson.toJson((StringType) fixed); - String escapedString = json.substring(json.indexOf(":") + 2); - escapedString = escapedString.substring(0, escapedString.indexOf(",'myStringValue") - 1); - expression.append("'" + escapedString + "'"); - } else if (fixed instanceof UriType) { - expression.append("'" + ((UriType) fixed).asStringValue() + "'"); - } else if (fixed instanceof IntegerType) { - expression.append(((IntegerType) fixed).asStringValue()); - } else if (fixed instanceof DecimalType) { - expression.append(((IntegerType) fixed).asStringValue()); - } else if (fixed instanceof BooleanType) { - expression.append(((BooleanType) fixed).asStringValue()); + List criteriaElements = getCriteriaForDiscriminator(path, ed, discriminator, profile, s.getType() == DiscriminatorType.PROFILE); + boolean found = false; + for (ElementDefinition criteriaElement : criteriaElements) { + found = true; + if (s.getType() == DiscriminatorType.TYPE) { + String type = null; + if (!criteriaElement.getPath().contains("[") && discriminator.contains("[")) { + discriminator = discriminator.substring(0, discriminator.indexOf('[')); + String lastNode = tail(discriminator); + type = tail(criteriaElement.getPath()).substring(lastNode.length()); + type = type.substring(0, 1).toLowerCase() + type.substring(1); + } else if (!criteriaElement.hasType() || criteriaElement.getType().size() == 1) { + if (discriminator.contains("[")) + discriminator = discriminator.substring(0, discriminator.indexOf('[')); + type = criteriaElement.getType().get(0).getWorkingCode(); + } else if (criteriaElement.getType().size() > 1) { + throw new DefinitionException("Discriminator (" + discriminator + ") is based on type, but slice " + ed.getId() + " in " + profile.getUrl() + " has multiple types: " + criteriaElement.typeSummary()); } else - throw new DefinitionException("Unsupported fixed value type for discriminator(" + discriminator + ") for slice " + ed.getId() + ": " + fixed.getClass().getName()); - expression.append(" in " + discriminator + ")"); - } - } - - // checkSpecials = we're only going to run these tests if we are actually validating this content (as opposed to we looked it up) - private void start(ValidatorHostContext hostContext, List errors, Element resource, Element element, StructureDefinition defn, NodeStack stack) throws FHIRException { - checkLang(resource, stack); - - if ("Bundle".equals(element.fhirType())) { - resolveBundleReferences(element, new ArrayList()); - } - startInner(hostContext, errors, resource, element, defn, stack, hostContext.isCheckSpecials()); - - List res = new ArrayList<>(); - Element meta = element.getNamedChild("meta"); - if (meta != null) { - List profiles = new ArrayList(); - meta.getNamedChildren("profile", profiles); - int i = 0; - for (Element profile : profiles) { - StructureDefinition sd = context.fetchResource(StructureDefinition.class, profile.primitiveValue()); - if (!defn.getUrl().equals(profile.primitiveValue())) { - if (warning(errors, IssueType.STRUCTURE, element.line(), element.col(), stack.getLiteralPath() + ".meta.profile[" + i + "]", sd != null, "Profile reference '" + profile.primitiveValue() + "' could not be resolved, so has not been checked")) { - startInner(hostContext, errors, resource, element, sd, stack, false); - } - } - i++; - } - } - } - - private void resolveBundleReferences(Element element, List bundles) { - if (!element.hasUserData("validator.bundle.resolved")) { - element.setUserData("validator.bundle.resolved", true); - List list = new ArrayList(); - list.addAll(bundles); - list.add(0, element); - List entries = element.getChildrenByName("entry"); - for (Element entry : entries) { - String fu = entry.getChildValue("fullUrl"); - Element r = entry.getNamedChild("resource"); - if (r != null) { - resolveBundleReferencesInResource(list, r, fu); - } - } - } - } - - private void resolveBundleReferencesInResource(List bundles, Element r, String fu) { - r.setUserData("validator.bundle.resolution-resource", null); - if ("Bundle".equals(r.fhirType())) { - resolveBundleReferences(r, bundles); - } else { - for (Element child : r.getChildren()) { - resolveBundleReferencesForElement(bundles, r, fu, child); - } - } - } - - private void resolveBundleReferencesForElement(List bundles, Element resource, String fu, Element element) { - if ("Reference".equals(element.fhirType())) { - String ref = element.getChildValue("reference"); - if (!Utilities.noString(ref)) { - for (Element bundle : bundles) { - List entries = bundle.getChildren("entry"); - Element tgt = resolveInBundle(entries, ref, fu, resource.fhirType(), resource.getIdBase()); - if (tgt != null) { - element.setUserData("validator.bundle.resolution", tgt.getNamedChild("resource")); - return; - } - } - element.setUserData("validator.bundle.resolution-failed", ref); - } - } else { - element.setUserData("validator.bundle.resolution-noref", null); - for (Element child : element.getChildren()) { - resolveBundleReferencesForElement(bundles, resource, fu, child); - } - } - - } - - public void startInner(ValidatorHostContext hostContext, List errors, Element resource, Element element, StructureDefinition defn, NodeStack stack, boolean checkSpecials) { - // the first piece of business is to see if we've validated this resource against this profile before. - // if we have (*or if we still are*), then we'll just return our existing errors - ResourceValidationTracker resTracker = getResourceTracker(element); - List cachedErrors = resTracker.getOutcomes(defn); - if (cachedErrors != null) { - for (ValidationMessage vm : cachedErrors) { - if (!errors.contains(vm)) { - errors.add(vm); - } - } - return; - } - if (rule(errors, IssueType.STRUCTURE, element.line(), element.col(), stack.getLiteralPath(), defn.hasSnapshot(), "StructureDefinition has no snapshot - validation is against the snapshot, so it must be provided")) { - List localErrors = new ArrayList(); - resTracker.startValidating(defn); - trackUsage(defn, hostContext, element); - validateElement(hostContext, localErrors, defn, defn.getSnapshot().getElement().get(0), null, null, resource, element, element.getName(), stack, false, true, null); - resTracker.storeOutcomes(defn, localErrors); - for (ValidationMessage vm : localErrors) { - if (!errors.contains(vm)) { - errors.add(vm); - } - } - } - if (checkSpecials) { - checkSpecials(hostContext, errors, element, stack, checkSpecials); - validateResourceRules(errors, element, stack); - } - } - - public void checkSpecials(ValidatorHostContext hostContext, List errors, Element element, NodeStack stack, boolean checkSpecials) { - // specific known special validations - if (element.getType().equals("Bundle")) { - validateBundle(errors, element, stack, checkSpecials); - } else if (element.getType().equals("Observation")) { - validateObservation(errors, element, stack); - } else if (element.getType().equals("Questionnaire")) { - ArrayList parents = new ArrayList<>(); - parents.add(element); - validateQuestionannaireItem(errors, element, element, stack, parents); - } else if (element.getType().equals("QuestionnaireResponse")) { - validateQuestionannaireResponse(hostContext, errors, element, stack); - } else if (element.getType().equals("CapabilityStatement")) { - validateCapabilityStatement(errors, element, stack); - } else if (element.getType().equals("CodeSystem")) { - validateCodeSystem(errors, element, stack); - } - } - - private ResourceValidationTracker getResourceTracker(Element element) { - ResourceValidationTracker res = resourceTracker.get(element); - if (res == null) { - res = new ResourceValidationTracker(); - resourceTracker.put(element, res); - } - return res; - } - - private void validateQuestionannaireItem(List errors, Element element, Element questionnaire, NodeStack stack, List parents) { - List list = getItems(element); - for (int i = 0; i < list.size(); i++) { - Element e = list.get(i); - NodeStack ns = stack.push(e, i, e.getProperty().getDefinition(), e.getProperty().getDefinition()); - validateQuestionnaireElement(errors, ns, questionnaire, e, parents); - List np = new ArrayList(); - np.add(e); - np.addAll(parents); - validateQuestionannaireItem(errors, e, questionnaire, ns, np); - } - } - - private void validateQuestionnaireElement(List errors, NodeStack ns, Element questionnaire, Element item, List parents) { - // R4+ - if ((FHIRVersion.isR4Plus(context.getVersion())) && (item.hasChildren("enableWhen"))) { - List ewl = item.getChildren("enableWhen"); - for (Element ew : ewl) { - String ql = ew.getNamedChildValue("question"); - if (rule(errors, IssueType.BUSINESSRULE, ns.literalPath, ql != null, "Questions with an enableWhen must have a value for the question link")) { - Element tgt = getQuestionById(item, ql); - if (rule(errors, IssueType.BUSINESSRULE, ns.literalPath, tgt == null, "Questions with an enableWhen cannot refer to an inner question for it's enableWhen condition")) { - tgt = getQuestionById(questionnaire, ql); - if (rule(errors, IssueType.BUSINESSRULE, ns.literalPath, tgt != null, "Unable to find target '" + ql + "' for this question enableWhen")) { - if (rule(errors, IssueType.BUSINESSRULE, ns.literalPath, tgt != item, "Target for this question enableWhen can't reference itself")) { - if (!isBefore(item, tgt, parents)) { - warning(errors, IssueType.BUSINESSRULE, ns.literalPath, false, "The target of this enableWhen rule (" + ql + ") comes after the question itself"); - } - } - } - } - } - } - } - } - - private boolean isBefore(Element item, Element tgt, List parents) { - // we work up the list, looking for tgt in the children of the parents - if (parents.contains(tgt)) { - // actually, if the target is a parent, that's automatically ok - return true; - } - for (Element p : parents) { - int i = findIndex(p, item); - int t = findIndex(p, tgt); - if (i > -1 && t > -1) { - return i > t; - } - } - return false; // unsure... shouldn't ever get to this point; - } - - - private int findIndex(Element parent, Element descendant) { - for (int i = 0; i < parent.getChildren().size(); i++) { - if (parent.getChildren().get(i) == descendant || isChild(parent.getChildren().get(i), descendant)) - return i; - } - return -1; - } - - private boolean isChild(Element element, Element descendant) { - for (Element e : element.getChildren()) { - if (e == descendant) - return true; - if (isChild(e, descendant)) - return true; - } - return false; - } - - private Element getQuestionById(Element focus, String ql) { - List list = getItems(focus); - for (Element item : list) { - String v = item.getNamedChildValue("linkId"); - if (ql.equals(v)) - return item; - Element tgt = getQuestionById(item, ql); - if (tgt != null) - return tgt; - } - return null; - - } - - private List getItems(Element element) { - List list = new ArrayList<>(); - element.getNamedChildren("item", list); - return list; - } - - private void checkLang(Element resource, NodeStack stack) { - String lang = resource.getNamedChildValue("language"); - if (!Utilities.noString(lang)) - stack.workingLang = lang; - } - - private void validateResourceRules(List errors, Element element, NodeStack stack) { - String lang = element.getNamedChildValue("language"); - Element text = element.getNamedChild("text"); - if (text != null) { - Element div = text.getNamedChild("div"); - if (lang != null && div != null) { - XhtmlNode xhtml = div.getXhtml(); - String l = xhtml.getAttribute("lang"); - String xl = xhtml.getAttribute("xml:lang"); - if (l == null && xl == null) { - warning(errors, IssueType.BUSINESSRULE, div.line(), div.col(), stack.getLiteralPath(), false, "Resource has a language, but the XHTML does not have an lang or an xml:lang tag (needs both - see https://www.w3.org/TR/i18n-html-tech-lang/#langvalues)"); - } else { - if (l == null) { - warning(errors, IssueType.BUSINESSRULE, div.line(), div.col(), stack.getLiteralPath(), false, "Resource has a language, but the XHTML does not have a lang tag (needs both lang and xml:lang - see https://www.w3.org/TR/i18n-html-tech-lang/#langvalues)"); - } else if (!l.equals(lang)) { - warning(errors, IssueType.BUSINESSRULE, div.line(), div.col(), stack.getLiteralPath(), false, "Resource has a language (" + lang + "), and the XHTML has a lang (" + l + "), but they differ "); - } - if (xl == null) { - warning(errors, IssueType.BUSINESSRULE, div.line(), div.col(), stack.getLiteralPath(), false, "Resource has a language, but the XHTML does not have an xml:lang tag (needs both lang and xml:lang - see https://www.w3.org/TR/i18n-html-tech-lang/#langvalues)"); - } else if (!xl.equals(lang)) { - warning(errors, IssueType.BUSINESSRULE, div.line(), div.col(), stack.getLiteralPath(), false, "Resource has a language (" + lang + "), and the XHTML has an xml:lang (" + xl + "), but they differ "); - } - } - } - } - // security tags are a set (system|code) - Element meta = element.getNamedChild("meta"); - if (meta != null) { - Set tags = new HashSet<>(); - List list = new ArrayList<>(); - meta.getNamedChildren("security", list); - int i = 0; - for (Element e : list) { - String s = e.getNamedChildValue("system") + "#" + e.getNamedChildValue("code"); - rule(errors, IssueType.BUSINESSRULE, e.line(), e.col(), stack.getLiteralPath() + ".meta.profile[" + Integer.toString(i) + "]", !tags.contains(s), "Duplicate Security Label " + s); - tags.add(s); - i++; - } - } - } - - private void validateCapabilityStatement(List errors, Element cs, NodeStack stack) { - int iRest = 0; - for (Element rest : cs.getChildrenByName("rest")) { - int iResource = 0; - for (Element resource : rest.getChildrenByName("resource")) { - int iSP = 0; - for (Element searchParam : resource.getChildrenByName("searchParam")) { - String ref = searchParam.getChildValue("definition"); - String type = searchParam.getChildValue("type"); - if (!Utilities.noString(ref)) { - SearchParameter sp = context.fetchResource(SearchParameter.class, ref); - if (sp != null) { - rule(errors, IssueType.INVALID, searchParam.line(), searchParam.col(), stack.literalPath + ".rest[" + iRest + "].resource[" + iResource + "].searchParam[" + iSP + "]", - sp.getType().toCode().equals(type), "Type mismatch - SearchParameter '" + sp.getUrl() + "' type is " + sp.getType().toCode() + ", but type here is " + type); - } - } - iSP++; - } - iResource++; - } - iRest++; - } - } - - private void validateCodeSystem(List errors, Element cs, NodeStack stack) { - String url = cs.getNamedChildValue("url"); - String vsu = cs.getNamedChildValue("valueSet"); - if (!Utilities.noString(vsu)) { - ValueSet vs; - try { - vs = context.fetchResourceWithException(ValueSet.class, vsu); - } catch (FHIRException e) { - vs = null; - } - if (vs != null) { - if (rule(errors, IssueType.BUSINESSRULE, stack.getLiteralPath(), vs.hasCompose() && !vs.hasExpansion(), "CodeSystem " + url + " has a 'all system' value set of " + vsu + ", but it is an expansion")) - if (rule(errors, IssueType.BUSINESSRULE, stack.getLiteralPath(), vs.getCompose().getInclude().size() == 1, "CodeSystem " + url + " has a 'all system' value set of " + vsu + ", but doesn't have a single include")) - if (rule(errors, IssueType.BUSINESSRULE, stack.getLiteralPath(), vs.getCompose().getInclude().get(0).getSystem().equals(url), "CodeSystem " + url + " has a 'all system' value set of " + vsu + ", but doesn't have a matching system (" + vs.getCompose().getInclude().get(0).getSystem() + ")")) { - rule(errors, IssueType.BUSINESSRULE, stack.getLiteralPath(), !vs.getCompose().getInclude().get(0).hasValueSet() - && !vs.getCompose().getInclude().get(0).hasConcept() && !vs.getCompose().getInclude().get(0).hasFilter(), "CodeSystem " + url + " has a 'all system' value set of " + vsu + ", but the include has extra details"); - } - } - } // todo... try getting the value set the other way... - } - - private void validateQuestionannaireResponse(ValidatorHostContext hostContext, List errors, Element element, NodeStack stack) throws FHIRException { - Element q = element.getNamedChild("questionnaire"); - String questionnaire = null; - if (q != null) { - /* - * q.getValue() is correct for R4 content, but we'll also accept the second - * option just in case we're validating raw STU3 content. Being lenient here - * isn't the end of the world since if someone is actually doing the reference - * wrong in R4 content it'll get flagged elsewhere by the validator too - */ - if (isNotBlank(q.getValue())) { - questionnaire = q.getValue(); - } else if (isNotBlank(q.getChildValue("reference"))) { - questionnaire = q.getChildValue("reference"); - } - } - if (hint(errors, IssueType.REQUIRED, element.line(), element.col(), stack.getLiteralPath(), questionnaire != null, "No questionnaire is identified, so no validation can be performed against the base questionnaire")) { - long t = System.nanoTime(); - Questionnaire qsrc = questionnaire.startsWith("#") ? loadQuestionnaire(element, questionnaire.substring(1)) : context.fetchResource(Questionnaire.class, questionnaire); - sdTime = sdTime + (System.nanoTime() - t); - if (warning(errors, IssueType.REQUIRED, q.line(), q.col(), stack.getLiteralPath(), qsrc != null, "The questionnaire '" + questionnaire + "' could not be resolved, so no validation can be performed against the base questionnaire")) { - boolean inProgress = "in-progress".equals(element.getNamedChildValue("status")); - validateQuestionannaireResponseItems(hostContext, qsrc, qsrc.getItem(), errors, element, stack, inProgress, element, new QStack(qsrc, element)); - } - } - } - - private Questionnaire loadQuestionnaire(Element resource, String id) throws FHIRException { - try { - for (Element contained : resource.getChildren("contained")) { - if (contained.getIdBase().equals(id)) { - FhirPublication v = FhirPublication.fromCode(context.getVersion()); - ByteArrayOutputStream bs = new ByteArrayOutputStream(); - new JsonParser(context).compose(contained, bs, OutputStyle.NORMAL, id); - byte[] json = bs.toByteArray(); - switch (v) { - case DSTU1: - throw new FHIRException("Unsupported version R1"); - case DSTU2: - org.hl7.fhir.dstu2.model.Resource r2 = new org.hl7.fhir.dstu2.formats.JsonParser().parse(json); - Resource r5 = VersionConvertor_10_50.convertResource(r2); - if (r5 instanceof Questionnaire) - return (Questionnaire) r5; - else - return null; - case DSTU2016May: - org.hl7.fhir.dstu2016may.model.Resource r2a = new org.hl7.fhir.dstu2016may.formats.JsonParser().parse(json); - r5 = VersionConvertor_14_50.convertResource(r2a); - if (r5 instanceof Questionnaire) - return (Questionnaire) r5; - else - return null; - case STU3: - org.hl7.fhir.dstu3.model.Resource r3 = new org.hl7.fhir.dstu3.formats.JsonParser().parse(json); - r5 = VersionConvertor_30_50.convertResource(r3, false); - if (r5 instanceof Questionnaire) - return (Questionnaire) r5; - else - return null; - case R4: - org.hl7.fhir.r4.model.Resource r4 = new org.hl7.fhir.r4.formats.JsonParser().parse(json); - r5 = VersionConvertor_40_50.convertResource(r4); - if (r5 instanceof Questionnaire) - return (Questionnaire) r5; - else - return null; - case R5: - r5 = new org.hl7.fhir.r5.formats.JsonParser().parse(json); - if (r5 instanceof Questionnaire) - return (Questionnaire) r5; - else - return null; - } - } - } - return null; - } catch (IOException e) { - throw new FHIRException(e); - } - } - - private void validateQuestionnaireResponseItem(ValidatorHostContext hostContext, Questionnaire qsrc, QuestionnaireItemComponent qItem, List errors, Element element, NodeStack stack, boolean inProgress, Element questionnaireResponseRoot, QStack qstack) { - String text = element.getNamedChildValue("text"); - rule(errors, IssueType.INVALID, element.line(), element.col(), stack.getLiteralPath(), Utilities.noString(text) || text.equals(qItem.getText()), "If text exists, it must match the questionnaire definition for linkId " + qItem.getLinkId()); - - List answers = new ArrayList(); - element.getNamedChildren("answer", answers); - if (inProgress) - warning(errors, IssueType.REQUIRED, element.line(), element.col(), stack.getLiteralPath(), isAnswerRequirementFulfilled(qItem, answers), "No response answer found for required item " + qItem.getLinkId()); - else if (myEnableWhenEvaluator.isQuestionEnabled(hostContext, qItem, qstack, fpe)) { - rule(errors, IssueType.REQUIRED, element.line(), element.col(), stack.getLiteralPath(), isAnswerRequirementFulfilled(qItem, answers), "No response answer found for required item " + qItem.getLinkId()); - } else if (!answers.isEmpty()) { // items without answers should be allowed, but not items with answers to questions that are disabled - // it appears that this is always a duplicate error - it will always already have beeb reported, so no need to report it again? - // GDG 2019-07-13 -// rule(errors, IssueType.INVALID, element.line(), element.col(), stack.getLiteralPath(), !isAnswerRequirementFulfilled(qItem, answers), "Item has answer (2), even though it is not enabled "+qItem.getLinkId()); - } - - if (answers.size() > 1) - rule(errors, IssueType.INVALID, answers.get(1).line(), answers.get(1).col(), stack.getLiteralPath(), qItem.getRepeats(), "Only one response answer item with this linkId allowed"); - - for (Element answer : answers) { - NodeStack ns = stack.push(answer, -1, null, null); - if (qItem.getType() != null) { - switch (qItem.getType()) { - case GROUP: - rule(errors, IssueType.STRUCTURE, answer.line(), answer.col(), stack.getLiteralPath(), false, "Items of type group should not have answers"); - break; - case DISPLAY: // nothing - break; - case BOOLEAN: - validateQuestionnaireResponseItemType(errors, answer, ns, "boolean"); - break; - case DECIMAL: - validateQuestionnaireResponseItemType(errors, answer, ns, "decimal"); - break; - case INTEGER: - validateQuestionnaireResponseItemType(errors, answer, ns, "integer"); - break; - case DATE: - validateQuestionnaireResponseItemType(errors, answer, ns, "date"); - break; - case DATETIME: - validateQuestionnaireResponseItemType(errors, answer, ns, "dateTime"); - break; - case TIME: - validateQuestionnaireResponseItemType(errors, answer, ns, "time"); - break; - case STRING: - validateQuestionnaireResponseItemType(errors, answer, ns, "string"); - break; - case TEXT: - validateQuestionnaireResponseItemType(errors, answer, ns, "text"); - break; - case URL: - validateQuestionnaireResponseItemType(errors, answer, ns, "uri"); - break; - case ATTACHMENT: - validateQuestionnaireResponseItemType(errors, answer, ns, "Attachment"); - break; - case REFERENCE: - validateQuestionnaireResponseItemType(errors, answer, ns, "Reference"); - break; - case QUANTITY: - if ("Quantity".equals(validateQuestionnaireResponseItemType(errors, answer, ns, "Quantity"))) - if (qItem.hasExtension("???")) - validateQuestionnaireResponseItemQuantity(errors, answer, ns); - break; - case CHOICE: - String itemType = validateQuestionnaireResponseItemType(errors, answer, ns, "Coding", "date", "time", "integer", "string"); - if (itemType != null) { - if (itemType.equals("Coding")) validateAnswerCode(errors, answer, ns, qsrc, qItem, false); - else if (itemType.equals("date")) checkOption(errors, answer, ns, qsrc, qItem, "date"); - else if (itemType.equals("time")) checkOption(errors, answer, ns, qsrc, qItem, "time"); - else if (itemType.equals("integer")) - checkOption(errors, answer, ns, qsrc, qItem, "integer"); - else if (itemType.equals("string")) checkOption(errors, answer, ns, qsrc, qItem, "string"); - } - break; - case OPENCHOICE: - itemType = validateQuestionnaireResponseItemType(errors, answer, ns, "Coding", "date", "time", "integer", "string"); - if (itemType != null) { - if (itemType.equals("Coding")) validateAnswerCode(errors, answer, ns, qsrc, qItem, true); - else if (itemType.equals("date")) checkOption(errors, answer, ns, qsrc, qItem, "date"); - else if (itemType.equals("time")) checkOption(errors, answer, ns, qsrc, qItem, "time"); - else if (itemType.equals("integer")) - checkOption(errors, answer, ns, qsrc, qItem, "integer"); - else if (itemType.equals("string")) - checkOption(errors, answer, ns, qsrc, qItem, "string", true); - } - break; -// case QUESTION: - case NULL: - // no validation - break; - } - } - validateQuestionannaireResponseItems(hostContext, qsrc, qItem.getItem(), errors, answer, stack, inProgress, questionnaireResponseRoot, qstack); - } - if (qItem.getType() == null) { - fail(errors, IssueType.REQUIRED, element.line(), element.col(), stack.getLiteralPath(), false, "Definition for item " + qItem.getLinkId() + " does not contain a type"); - } else if (qItem.getType() == QuestionnaireItemType.DISPLAY) { - List items = new ArrayList(); - element.getNamedChildren("item", items); - rule(errors, IssueType.STRUCTURE, element.line(), element.col(), stack.getLiteralPath(), items.isEmpty(), "Items not of type DISPLAY should not have items - linkId {0}", qItem.getLinkId()); - } else { - validateQuestionannaireResponseItems(hostContext, qsrc, qItem.getItem(), errors, element, stack, inProgress, questionnaireResponseRoot, qstack); - } - } - - private boolean isAnswerRequirementFulfilled(QuestionnaireItemComponent qItem, List answers) { - return !answers.isEmpty() || !qItem.getRequired() || qItem.getType() == QuestionnaireItemType.GROUP; - } - - private void validateQuestionnaireResponseItem(ValidatorHostContext hostcontext, Questionnaire qsrc, QuestionnaireItemComponent qItem, List errors, List elements, NodeStack stack, boolean inProgress, Element questionnaireResponseRoot, QStack qstack) { - if (elements.size() > 1) - rule(errors, IssueType.INVALID, elements.get(1).line(), elements.get(1).col(), stack.getLiteralPath(), qItem.getRepeats(), "Only one response item with this linkId allowed - " + qItem.getLinkId()); - int i = 0; - for (Element element : elements) { - NodeStack ns = stack.push(element, i, null, null); - validateQuestionnaireResponseItem(hostcontext, qsrc, qItem, errors, element, ns, inProgress, questionnaireResponseRoot, qstack.push(qItem, element)); - i++; - } - } - - private int getLinkIdIndex(List qItems, String linkId) { - for (int i = 0; i < qItems.size(); i++) { - if (linkId.equals(qItems.get(i).getLinkId())) - return i; - } - return -1; - } - - private void validateQuestionannaireResponseItems(ValidatorHostContext hostContext, Questionnaire qsrc, List qItems, List errors, Element element, NodeStack stack, boolean inProgress, Element questionnaireResponseRoot, QStack qstack) { - List items = new ArrayList(); - element.getNamedChildren("item", items); - // now, sort into stacks - Map> map = new HashMap>(); - int lastIndex = -1; - for (Element item : items) { - String linkId = item.getNamedChildValue("linkId"); - if (rule(errors, IssueType.REQUIRED, item.line(), item.col(), stack.getLiteralPath(), !Utilities.noString(linkId), "No LinkId, so can't be validated")) { - int index = getLinkIdIndex(qItems, linkId); - if (index == -1) { - QuestionnaireItemComponent qItem = findQuestionnaireItem(qsrc, linkId); - if (qItem != null) { - rule(errors, IssueType.STRUCTURE, item.line(), item.col(), stack.getLiteralPath(), index > -1, misplacedItemError(qItem)); - NodeStack ns = stack.push(item, -1, null, null); - validateQuestionnaireResponseItem(hostContext, qsrc, qItem, errors, item, ns, inProgress, questionnaireResponseRoot, qstack.push(qItem, item)); - } else - rule(errors, IssueType.NOTFOUND, item.line(), item.col(), stack.getLiteralPath(), index > -1, "LinkId '" + linkId + "' not found in questionnaire"); - } else { - rule(errors, IssueType.STRUCTURE, item.line(), item.col(), stack.getLiteralPath(), index >= lastIndex, "Structural Error: items are out of order"); - lastIndex = index; - - // If an item has a child called "linkId" but no child called "answer", - // we'll treat it as not existing for the purposes of enableWhen validation - if (item.hasChildren("answer") || item.hasChildren("item")) { - List mapItem = map.computeIfAbsent(linkId, key -> new ArrayList<>()); - mapItem.add(item); - } - } - } - } - - // ok, now we have a list of known items, grouped by linkId. We've made an error for anything out of order - for (QuestionnaireItemComponent qItem : qItems) { - List mapItem = map.get(qItem.getLinkId()); - validateQuestionnaireResponseItem(hostContext, qsrc, errors, element, stack, inProgress, questionnaireResponseRoot, qItem, mapItem, qstack); - } - } - - public void validateQuestionnaireResponseItem(ValidatorHostContext hostContext, Questionnaire qsrc, List errors, Element element, NodeStack stack, boolean inProgress, Element questionnaireResponseRoot, QuestionnaireItemComponent qItem, List mapItem, QStack qstack) { - boolean enabled = myEnableWhenEvaluator.isQuestionEnabled(hostContext, qItem, qstack, fpe); - if (mapItem != null) { - if (!enabled) { - int i = 0; - for (Element e : mapItem) { - NodeStack ns = stack.push(e, i, e.getProperty().getDefinition(), e.getProperty().getDefinition()); - rule(errors, IssueType.INVALID, e.line(), e.col(), ns.getLiteralPath(), enabled, "Item has answer, even though it is not enabled (item id = '" + qItem.getLinkId() + "')"); - i++; - } - } - - // Recursively validate child items - validateQuestionnaireResponseItem(hostContext, qsrc, qItem, errors, mapItem, stack, inProgress, questionnaireResponseRoot, qstack); - - } else { - - // item is missing, is the question enabled? - if (enabled && qItem.getRequired()) { - String message = "No response found for required item with id = '" + qItem.getLinkId() + "'"; - if (inProgress) { - warning(errors, IssueType.REQUIRED, element.line(), element.col(), stack.getLiteralPath(), false, message); - } else { - rule(errors, IssueType.REQUIRED, element.line(), element.col(), stack.getLiteralPath(), false, message); - } - } - - } - - } - - private String misplacedItemError(QuestionnaireItemComponent qItem) { - return qItem.hasLinkId() ? String.format("Structural Error: item with linkid %s is in the wrong place", qItem.getLinkId()) : "Structural Error: item is in the wrong place"; - } - - private void validateQuestionnaireResponseItemQuantity(List errors, Element answer, NodeStack stack) { - - } - - private String validateQuestionnaireResponseItemType(List errors, Element element, NodeStack stack, String... types) { - List values = new ArrayList(); - element.getNamedChildrenWithWildcard("value[x]", values); - for (int i = 0; i < types.length; i++) { - if (types[i].equals("text")) { - types[i] = "string"; - } - } - if (values.size() > 0) { - NodeStack ns = stack.push(values.get(0), -1, null, null); - CommaSeparatedStringBuilder l = new CommaSeparatedStringBuilder(); - for (String s : types) { - l.append(s); - if (values.get(0).getName().equals("value" + Utilities.capitalize(s))) - return (s); - } - if (types.length == 1) - rule(errors, IssueType.STRUCTURE, values.get(0).line(), values.get(0).col(), ns.getLiteralPath(), false, "Answer value must be of type " + types[0]); + throw new DefinitionException("Discriminator (" + discriminator + ") is based on type, but slice " + ed.getId() + " in " + profile.getUrl() + " has no types"); + if (discriminator.isEmpty()) + expression.append(" and $this is " + type); else - rule(errors, IssueType.STRUCTURE, values.get(0).line(), values.get(0).col(), ns.getLiteralPath(), false, "Answer value must be one of the types " + l.toString()); - } - return null; - } - - private QuestionnaireItemComponent findQuestionnaireItem(Questionnaire qSrc, String linkId) { - return findItem(qSrc.getItem(), linkId); - } - - private QuestionnaireItemComponent findItem(List list, String linkId) { - for (QuestionnaireItemComponent item : list) { - if (linkId.equals(item.getLinkId())) - return item; - QuestionnaireItemComponent result = findItem(item.getItem(), linkId); - if (result != null) - return result; - } - return null; - } - - private void validateAnswerCode(List errors, Element value, NodeStack stack, Questionnaire qSrc, String ref, boolean theOpenChoice) { - ValueSet vs = resolveBindingReference(qSrc, ref, qSrc.getUrl()); - if (warning(errors, IssueType.CODEINVALID, value.line(), value.col(), stack.getLiteralPath(), vs != null, "ValueSet " + describeReference(ref) + " not found by validator")) { - try { - Coding c = ObjectConverter.readAsCoding(value); - if (isBlank(c.getCode()) && isBlank(c.getSystem()) && isNotBlank(c.getDisplay())) { - if (theOpenChoice) { - return; - } - } - - long t = System.nanoTime(); - ValidationResult res = context.validateCode(new ValidationOptions(stack.workingLang), c, vs); - txTime = txTime + (System.nanoTime() - t); - if (!res.isOk()) { - txRule(errors, res.getTxLink(), IssueType.CODEINVALID, value.line(), value.col(), stack.getLiteralPath(), false, "The value provided (" + c.getSystem() + "::" + c.getCode() + ") is not in the options value set in the questionnaire"); - } else if (res.getSeverity() != null) { - super.addValidationMessage(errors, IssueType.CODEINVALID, value.line(), value.col(), stack.getLiteralPath(), res.getMessage(), res.getSeverity(), Source.TerminologyEngine); - } - } catch (Exception e) { - warning(errors, IssueType.CODEINVALID, value.line(), value.col(), stack.getLiteralPath(), false, "Error " + e.getMessage() + " validating Coding against Questionnaire Options"); + expression.append(" and " + discriminator + " is " + type); + } else if (s.getType() == DiscriminatorType.PROFILE) { + if (criteriaElement.getType().size() == 0) { + throw new DefinitionException("Profile based discriminators must have a type (" + criteriaElement.getId() + " in profile " + profile.getUrl() + ")"); } + if (criteriaElement.getType().size() != 1) { + throw new DefinitionException("Profile based discriminators must have only one type (" + criteriaElement.getId() + " in profile " + profile.getUrl() + ")"); + } + List list = discriminator.endsWith(".resolve()") || discriminator.equals("resolve()") ? criteriaElement.getType().get(0).getTargetProfile() : criteriaElement.getType().get(0).getProfile(); + if (list.size() == 0) { + throw new DefinitionException("Profile based discriminators must have a type with a profile (" + criteriaElement.getId() + " in profile " + profile.getUrl() + ")"); + } else if (list.size() > 1) { + CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder(" or "); + for (CanonicalType c : list) { + b.append(discriminator + ".conformsTo('" + c.getValue() + "')"); + } + expression.append(" and (" + b + ")"); + } else { + expression.append(" and " + discriminator + ".conformsTo('" + list.get(0).getValue() + "')"); + } + } else if (s.getType() == DiscriminatorType.EXISTS) { + if (criteriaElement.hasMin() && criteriaElement.getMin() >= 1) + expression.append(" and (" + discriminator + ".exists())"); + else if (criteriaElement.hasMax() && criteriaElement.getMax().equals("0")) + expression.append(" and (" + discriminator + ".exists().not())"); + else + throw new FHIRException("Discriminator (" + discriminator + ") is based on element existence, but slice " + ed.getId() + " neither sets min>=1 or max=0"); + } else if (criteriaElement.hasFixed()) { + buildFixedExpression(ed, expression, discriminator, criteriaElement); + } else if (criteriaElement.hasPattern()) { + buildPattternExpression(ed, expression, discriminator, criteriaElement); + } else if (criteriaElement.hasBinding() && criteriaElement.getBinding().hasStrength() && criteriaElement.getBinding().getStrength().equals(BindingStrength.REQUIRED) && criteriaElement.getBinding().hasValueSet()) { + expression.append(" and (" + discriminator + " memberOf '" + criteriaElement.getBinding().getValueSet() + "')"); + } else { + found = false; + } + if (found) + break; } - } - - private void validateAnswerCode(List errors, Element answer, NodeStack stack, Questionnaire qSrc, QuestionnaireItemComponent qItem, boolean theOpenChoice) { - Element v = answer.getNamedChild("valueCoding"); - NodeStack ns = stack.push(v, -1, null, null); - if (qItem.getAnswerOption().size() > 0) - checkCodingOption(errors, answer, stack, qSrc, qItem, theOpenChoice); - // validateAnswerCode(errors, v, stack, qItem.getOption()); - else if (qItem.hasAnswerValueSet()) - validateAnswerCode(errors, v, stack, qSrc, qItem.getAnswerValueSet(), theOpenChoice); + if (found) + anyFound = true; + } + if (!anyFound) { + if (slicer.getSlicing().getDiscriminator().size() > 1) + throw new DefinitionException("Could not match any discriminators (" + discriminators + ") for slice " + ed.getId() + " in profile " + profile.getUrl() + " - None of the discriminator " + discriminators + " have fixed value, binding or existence assertions"); else - hint(errors, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), false, "Cannot validate options because no option or options are provided"); + throw new DefinitionException("Could not match discriminator (" + discriminators + ") for slice " + ed.getId() + " in profile " + profile.getUrl() + " - the discriminator " + discriminators + " does not have fixed value, binding or existence assertions"); + } + + try { + n = fpe.parse(fixExpr(expression.toString())); + } catch (FHIRLexerException e) { + throw new FHIRException("Problem processing expression " + expression + " in profile " + profile.getUrl() + " path " + path + ": " + e.getMessage()); + } + fpeTime = fpeTime + (System.nanoTime() - t); + ed.setUserData("slice.expression.cache", n); } - private void checkOption(List errors, Element answer, NodeStack stack, Questionnaire qSrc, QuestionnaireItemComponent qItem, String type) { - checkOption(errors, answer, stack, qSrc, qItem, type, false); + ValidatorHostContext shc = hostContext.forSlicing(); + boolean pass = evaluateSlicingExpression(shc, element, path, profile, n); + if (!pass) { + slicingHint(sliceInfo, IssueType.STRUCTURE, element.line(), element.col(), path, false, "Does not match slice'" + ed.getSliceName(), "discriminator = " + Utilities.escapeXml(n.toString())); + for (String url : shc.getSliceRecords().keySet()) { + slicingHint(sliceInfo, IssueType.STRUCTURE, element.line(), element.col(), path, false, "Details for " + stack.getLiteralPath() + " against profile " + url, + "Profile " + url + " does not match for " + stack.getLiteralPath() + " because of the following profile issues: " + errorSummaryForSlicingAsHtml(shc.getSliceRecords().get(url))); + } } + return pass; + } - private void checkOption(List errors, Element answer, NodeStack stack, Questionnaire qSrc, QuestionnaireItemComponent qItem, String type, boolean openChoice) { - if (type.equals("integer")) checkIntegerOption(errors, answer, stack, qSrc, qItem, openChoice); - else if (type.equals("date")) checkDateOption(errors, answer, stack, qSrc, qItem, openChoice); - else if (type.equals("time")) checkTimeOption(errors, answer, stack, qSrc, qItem, openChoice); - else if (type.equals("string")) checkStringOption(errors, answer, stack, qSrc, qItem, openChoice); - else if (type.equals("Coding")) checkCodingOption(errors, answer, stack, qSrc, qItem, openChoice); + public boolean evaluateSlicingExpression(ValidatorHostContext hostContext, Element element, String path, StructureDefinition profile, ExpressionNode n) throws FHIRException { + String msg; + boolean ok; + try { + long t = System.nanoTime(); + ok = fpe.evaluateToBoolean(hostContext.forProfile(profile), hostContext.getResource(), hostContext.getRootResource(), element, n); + fpeTime = fpeTime + (System.nanoTime() - t); + msg = fpe.forLog(); + } catch (Exception ex) { + ex.printStackTrace(); + throw new FHIRException("Problem evaluating slicing expression for element in profile " + profile.getUrl() + " path " + path + " (fhirPath = " + n + "): " + ex.getMessage()); } + return ok; + } - private void checkIntegerOption(List errors, Element answer, NodeStack stack, Questionnaire qSrc, QuestionnaireItemComponent qItem, boolean openChoice) { - Element v = answer.getNamedChild("valueInteger"); - NodeStack ns = stack.push(v, -1, null, null); - if (qItem.getAnswerOption().size() > 0) { - List list = new ArrayList(); - for (QuestionnaireItemAnswerOptionComponent components : qItem.getAnswerOption()) { - try { - list.add(components.getValueIntegerType()); - } catch (FHIRException e) { - // If it's the wrong type, just keep going - } - } - if (list.isEmpty() && !openChoice) { - rule(errors, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), false, "Option list has no option values of type integer"); - } else { - boolean found = false; - for (IntegerType item : list) { - if (item.getValue() == Integer.parseInt(v.primitiveValue())) { - found = true; - break; - } - } - if (!found) { - rule(errors, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), found, "The integer " + v.primitiveValue() + " is not a valid option"); - } - } - } else - hint(errors, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), false, "Cannot validate integer answer option because no option list is provided"); + private void buildPattternExpression(ElementDefinition ed, StringBuilder expression, String discriminator, ElementDefinition criteriaElement) throws DefinitionException { + DataType pattern = criteriaElement.getPattern(); + if (pattern instanceof CodeableConcept) { + CodeableConcept cc = (CodeableConcept) pattern; + expression.append(" and "); + buildCodeableConceptExpression(ed, expression, discriminator, cc); + } else if (pattern instanceof Identifier) { + Identifier ii = (Identifier) pattern; + expression.append(" and "); + buildIdentifierExpression(ed, expression, discriminator, ii); + } else + throw new DefinitionException("Unsupported fixed pattern type for discriminator(" + discriminator + ") for slice " + ed.getId() + ": " + pattern.getClass().getName()); + } + + private void buildIdentifierExpression(ElementDefinition ed, StringBuilder expression, String discriminator, Identifier ii) + throws DefinitionException { + if (ii.hasExtension()) + throw new DefinitionException("Unsupported Identifier pattern - extensions are not allowed - for discriminator(" + discriminator + ") for slice " + ed.getId()); + boolean first = true; + expression.append(discriminator + ".where("); + if (ii.hasSystem()) { + first = false; + expression.append("system = '" + ii.getSystem() + "'"); } - - private void checkDateOption(List errors, Element answer, NodeStack stack, Questionnaire qSrc, QuestionnaireItemComponent qItem, boolean openChoice) { - Element v = answer.getNamedChild("valueDate"); - NodeStack ns = stack.push(v, -1, null, null); - if (qItem.getAnswerOption().size() > 0) { - List list = new ArrayList(); - for (QuestionnaireItemAnswerOptionComponent components : qItem.getAnswerOption()) { - try { - list.add(components.getValueDateType()); - } catch (FHIRException e) { - // If it's the wrong type, just keep going - } - } - if (list.isEmpty() && !openChoice) { - rule(errors, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), false, "Option list has no option values of type date"); - } else { - boolean found = false; - for (DateType item : list) { - if (item.getValue().equals(v.primitiveValue())) { - found = true; - break; - } - } - if (!found) { - rule(errors, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), found, "The date " + v.primitiveValue() + " is not a valid option"); - } - } - } else - hint(errors, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), false, "Cannot validate date answer option because no option list is provided"); + if (ii.hasValue()) { + if (first) + first = false; + else + expression.append(" and "); + expression.append("value = '" + ii.getValue() + "'"); } - - private void checkTimeOption(List errors, Element answer, NodeStack stack, Questionnaire qSrc, QuestionnaireItemComponent qItem, boolean openChoice) { - Element v = answer.getNamedChild("valueTime"); - NodeStack ns = stack.push(v, -1, null, null); - if (qItem.getAnswerOption().size() > 0) { - List list = new ArrayList(); - for (QuestionnaireItemAnswerOptionComponent components : qItem.getAnswerOption()) { - try { - list.add(components.getValueTimeType()); - } catch (FHIRException e) { - // If it's the wrong type, just keep going - } - } - if (list.isEmpty() && !openChoice) { - rule(errors, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), false, "Option list has no option values of type time"); - } else { - boolean found = false; - for (TimeType item : list) { - if (item.getValue().equals(v.primitiveValue())) { - found = true; - break; - } - } - if (!found) { - rule(errors, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), found, "The time " + v.primitiveValue() + " is not a valid option"); - } - } - } else - hint(errors, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), false, "Cannot validate time answer option because no option list is provided"); + if (ii.hasUse()) { + if (first) + first = false; + else + expression.append(" and "); + expression.append("use = '" + ii.getUse() + "'"); } + if (ii.hasType()) { + if (first) + first = false; + else + expression.append(" and "); + buildCodeableConceptExpression(ed, expression, "type", ii.getType()); + } + expression.append(").exists()"); + } - private void checkStringOption(List errors, Element answer, NodeStack stack, Questionnaire qSrc, QuestionnaireItemComponent qItem, boolean openChoice) { - Element v = answer.getNamedChild("valueString"); - NodeStack ns = stack.push(v, -1, null, null); - if (qItem.getAnswerOption().size() > 0) { - List list = new ArrayList(); - for (QuestionnaireItemAnswerOptionComponent components : qItem.getAnswerOption()) { - try { - if (components.getValue() != null) { - list.add(components.getValueStringType()); - } - } catch (FHIRException e) { - // If it's the wrong type, just keep going - } - } - if (!openChoice) { - if (list.isEmpty()) { - rule(errors, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), false, "Option list has no option values of type string"); - } else { - boolean found = false; - for (StringType item : list) { - if (item.getValue().equals((v.primitiveValue()))) { - found = true; - break; - } - } - if (!found) { - rule(errors, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), found, "The string " + v.primitiveValue() + " is not a valid option"); - } - } - } - } else { - hint(errors, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), false, "Cannot validate string answer option because no option list is provided"); + private void buildCodeableConceptExpression(ElementDefinition ed, StringBuilder expression, String discriminator, CodeableConcept cc) + throws DefinitionException { + if (cc.hasText()) + throw new DefinitionException("Unsupported CodeableConcept pattern - using text - for discriminator(" + discriminator + ") for slice " + ed.getId()); + if (!cc.hasCoding()) + throw new DefinitionException("Unsupported CodeableConcept pattern - must have at least one coding - for discriminator(" + discriminator + ") for slice " + ed.getId()); + if (cc.hasExtension()) + throw new DefinitionException("Unsupported CodeableConcept pattern - extensions are not allowed - for discriminator(" + discriminator + ") for slice " + ed.getId()); + boolean firstCoding = true; + for (Coding c : cc.getCoding()) { + if (c.hasExtension()) + throw new DefinitionException("Unsupported CodeableConcept pattern - extensions are not allowed - for discriminator(" + discriminator + ") for slice " + ed.getId()); + if (firstCoding) firstCoding = false; + else expression.append(" and "); + expression.append(discriminator + ".coding.where("); + boolean first = true; + if (c.hasSystem()) { + first = false; + expression.append("system = '" + c.getSystem() + "'"); + } + if (c.hasVersion()) { + if (first) first = false; + else expression.append(" and "); + expression.append("version = '" + c.getVersion() + "'"); + } + if (c.hasCode()) { + if (first) first = false; + else expression.append(" and "); + expression.append("code = '" + c.getCode() + "'"); + } + if (c.hasDisplay()) { + if (first) first = false; + else expression.append(" and "); + expression.append("display = '" + c.getDisplay() + "'"); + } + expression.append(").exists()"); + } + } + + private void buildFixedExpression(ElementDefinition ed, StringBuilder expression, String discriminator, ElementDefinition criteriaElement) throws DefinitionException { + DataType fixed = criteriaElement.getFixed(); + if (fixed instanceof CodeableConcept) { + CodeableConcept cc = (CodeableConcept) fixed; + expression.append(" and "); + buildCodeableConceptExpression(ed, expression, discriminator, cc); + } else if (fixed instanceof Identifier) { + Identifier ii = (Identifier) fixed; + expression.append(" and "); + buildIdentifierExpression(ed, expression, discriminator, ii); + } else { + expression.append(" and ("); + if (fixed instanceof StringType) { + Gson gson = new Gson(); + String json = gson.toJson((StringType) fixed); + String escapedString = json.substring(json.indexOf(":") + 2); + escapedString = escapedString.substring(0, escapedString.indexOf(",'myStringValue") - 1); + expression.append("'" + escapedString + "'"); + } else if (fixed instanceof UriType) { + expression.append("'" + ((UriType) fixed).asStringValue() + "'"); + } else if (fixed instanceof IntegerType) { + expression.append(((IntegerType) fixed).asStringValue()); + } else if (fixed instanceof DecimalType) { + expression.append(((IntegerType) fixed).asStringValue()); + } else if (fixed instanceof BooleanType) { + expression.append(((BooleanType) fixed).asStringValue()); + } else + throw new DefinitionException("Unsupported fixed value type for discriminator(" + discriminator + ") for slice " + ed.getId() + ": " + fixed.getClass().getName()); + expression.append(" in " + discriminator + ")"); + } + } + + // checkSpecials = we're only going to run these tests if we are actually validating this content (as opposed to we looked it up) + private void start(ValidatorHostContext hostContext, List errors, Element resource, Element element, StructureDefinition defn, NodeStack stack) throws FHIRException { + checkLang(resource, stack); + + if ("Bundle".equals(element.fhirType())) { + resolveBundleReferences(element, new ArrayList()); + } + startInner(hostContext, errors, resource, element, defn, stack, hostContext.isCheckSpecials()); + + List res = new ArrayList<>(); + Element meta = element.getNamedChild("meta"); + if (meta != null) { + List profiles = new ArrayList(); + meta.getNamedChildren("profile", profiles); + int i = 0; + for (Element profile : profiles) { + StructureDefinition sd = context.fetchResource(StructureDefinition.class, profile.primitiveValue()); + if (!defn.getUrl().equals(profile.primitiveValue())) { + if (warning(errors, IssueType.STRUCTURE, element.line(), element.col(), stack.getLiteralPath() + ".meta.profile[" + i + "]", sd != null,messages.getString("Profile_reference__could_not_be_resolved_so_has_not_been_checked"), profile.primitiveValue())) { + startInner(hostContext, errors, resource, element, sd, stack, false); + } } + i++; + } } + } - private void checkCodingOption(List errors, Element answer, NodeStack stack, Questionnaire qSrc, QuestionnaireItemComponent qItem, boolean openChoice) { - Element v = answer.getNamedChild("valueCoding"); - String system = v.getNamedChildValue("system"); - String code = v.getNamedChildValue("code"); - NodeStack ns = stack.push(v, -1, null, null); - if (qItem.getAnswerOption().size() > 0) { - List list = new ArrayList(); - for (QuestionnaireItemAnswerOptionComponent components : qItem.getAnswerOption()) { - try { - if (components.getValue() != null) { - list.add(components.getValueCoding()); - } - } catch (FHIRException e) { - // If it's the wrong type, just keep going - } - } - if (list.isEmpty() && !openChoice) { - rule(errors, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), false, "Option list has no option values of type coding"); - } else { - boolean found = false; - for (Coding item : list) { - if (ObjectUtil.equals(item.getSystem(), system) && ObjectUtil.equals(item.getCode(), code)) { - found = true; - break; - } - } - if (!found) { - rule(errors, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), found, "The code " + system + "::" + code + " is not a valid option"); - } - } - } else - hint(errors, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), false, "Cannot validate Coding option because no option list is provided"); - } - - private String tail(String path) { - return path.substring(path.lastIndexOf(".") + 1); - } - - private String tryParse(String ref) { - String[] parts = ref.split("\\/"); - switch (parts.length) { - case 1: - return null; - case 2: - return checkResourceType(parts[0]); - default: - if (parts[parts.length - 2].equals("_history")) - return checkResourceType(parts[parts.length - 4]); - else - return checkResourceType(parts[parts.length - 2]); + private void resolveBundleReferences(Element element, List bundles) { + if (!element.hasUserData("validator.bundle.resolved")) { + element.setUserData("validator.bundle.resolved", true); + List list = new ArrayList(); + list.addAll(bundles); + list.add(0, element); + List entries = element.getChildrenByName("entry"); + for (Element entry : entries) { + String fu = entry.getChildValue("fullUrl"); + Element r = entry.getNamedChild("resource"); + if (r != null) { + resolveBundleReferencesInResource(list, r, fu); } + } + } + } + + private void resolveBundleReferencesInResource(List bundles, Element r, String fu) { + r.setUserData("validator.bundle.resolution-resource", null); + if ("Bundle".equals(r.fhirType())) { + resolveBundleReferences(r, bundles); + } else { + for (Element child : r.getChildren()) { + resolveBundleReferencesForElement(bundles, r, fu, child); + } + } + } + + private void resolveBundleReferencesForElement(List bundles, Element resource, String fu, Element element) { + if ("Reference".equals(element.fhirType())) { + String ref = element.getChildValue("reference"); + if (!Utilities.noString(ref)) { + for (Element bundle : bundles) { + List entries = bundle.getChildren("entry"); + Element tgt = resolveInBundle(entries, ref, fu, resource.fhirType(), resource.getIdBase()); + if (tgt != null) { + element.setUserData("validator.bundle.resolution", tgt.getNamedChild("resource")); + return; + } + } + element.setUserData("validator.bundle.resolution-failed", ref); + } + } else { + element.setUserData("validator.bundle.resolution-noref", null); + for (Element child : element.getChildren()) { + resolveBundleReferencesForElement(bundles, resource, fu, child); + } } - private boolean typesAreAllReference(List theType) { - for (TypeRefComponent typeRefComponent : theType) { - if (typeRefComponent.getCode().equals("Reference") == false) { - return false; - } + } + + public void startInner(ValidatorHostContext hostContext, List errors, Element resource, Element element, StructureDefinition defn, NodeStack stack, boolean checkSpecials) { + // the first piece of business is to see if we've validated this resource against this profile before. + // if we have (*or if we still are*), then we'll just return our existing errors + ResourceValidationTracker resTracker = getResourceTracker(element); + List cachedErrors = resTracker.getOutcomes(defn); + if (cachedErrors != null) { + for (ValidationMessage vm : cachedErrors) { + if (!errors.contains(vm)) { + errors.add(vm); } + } + return; + } + if (rule(errors, IssueType.STRUCTURE, element.line(), element.col(), stack.getLiteralPath(), defn.hasSnapshot(),messages.getString("StructureDefinition_has_no_snapshot__validation_is_against_the_snapshot_so_it_must_be_provided"))) { + List localErrors = new ArrayList(); + resTracker.startValidating(defn); + trackUsage(defn, hostContext, element); + validateElement(hostContext, localErrors, defn, defn.getSnapshot().getElement().get(0), null, null, resource, element, element.getName(), stack, false, true, null); + resTracker.storeOutcomes(defn, localErrors); + for (ValidationMessage vm : localErrors) { + if (!errors.contains(vm)) { + errors.add(vm); + } + } + } + if (checkSpecials) { + checkSpecials(hostContext, errors, element, stack, checkSpecials); + validateResourceRules(errors, element, stack); + } + } + + public void checkSpecials(ValidatorHostContext hostContext, List errors, Element element, NodeStack stack, boolean checkSpecials) { + // specific known special validations + if (element.getType().equals("Bundle")) { + validateBundle(errors, element, stack, checkSpecials); + } else if (element.getType().equals("Observation")) { + validateObservation(errors, element, stack); + } else if (element.getType().equals("Questionnaire")) { + ArrayList parents = new ArrayList<>(); + parents.add(element); + validateQuestionannaireItem(errors, element, element, stack, parents); + } else if (element.getType().equals("QuestionnaireResponse")) { + validateQuestionannaireResponse(hostContext, errors, element, stack); + } else if (element.getType().equals("CapabilityStatement")) { + validateCapabilityStatement(errors, element, stack); + } else if (element.getType().equals("CodeSystem")) { + validateCodeSystem(errors, element, stack); + } + } + + private ResourceValidationTracker getResourceTracker(Element element) { + ResourceValidationTracker res = resourceTracker.get(element); + if (res == null) { + res = new ResourceValidationTracker(); + resourceTracker.put(element, res); + } + return res; + } + + private void validateQuestionannaireItem(List errors, Element element, Element questionnaire, NodeStack stack, List parents) { + List list = getItems(element); + for (int i = 0; i < list.size(); i++) { + Element e = list.get(i); + NodeStack ns = stack.push(e, i, e.getProperty().getDefinition(), e.getProperty().getDefinition()); + validateQuestionnaireElement(errors, ns, questionnaire, e, parents); + List np = new ArrayList(); + np.add(e); + np.addAll(parents); + validateQuestionannaireItem(errors, e, questionnaire, ns, np); + } + } + + private void validateQuestionnaireElement(List errors, NodeStack ns, Element questionnaire, Element item, List parents) { + // R4+ + if ((FHIRVersion.isR4Plus(context.getVersion())) && (item.hasChildren("enableWhen"))) { + List ewl = item.getChildren("enableWhen"); + for (Element ew : ewl) { + String ql = ew.getNamedChildValue("question"); + if (rule(errors, IssueType.BUSINESSRULE, ns.literalPath, ql != null,messages.getString("Questions_with_an_enableWhen_must_have_a_value_for_the_question_link"))) { + Element tgt = getQuestionById(item, ql); + if (rule(errors, IssueType.BUSINESSRULE, ns.literalPath, tgt == null,messages.getString("Questions_with_an_enableWhen_cannot_refer_to_an_inner_question_for_its_enableWhen_condition"))) { + tgt = getQuestionById(questionnaire, ql); + if (rule(errors, IssueType.BUSINESSRULE, ns.literalPath, tgt != null,messages.getString("Unable_to_find_target__for_this_question_enableWhen"), ql)) { + if (rule(errors, IssueType.BUSINESSRULE, ns.literalPath, tgt != item,messages.getString("Target_for_this_question_enableWhen_cant_reference_itself"))) { + if (!isBefore(item, tgt, parents)) { + warning(errors, IssueType.BUSINESSRULE, ns.literalPath, false,messages.getString("The_target_of_this_enableWhen_rule__comes_after_the_question_itself"), ql); + } + } + } + } + } + } + } + } + + private boolean isBefore(Element item, Element tgt, List parents) { + // we work up the list, looking for tgt in the children of the parents + if (parents.contains(tgt)) { + // actually, if the target is a parent, that's automatically ok + return true; + } + for (Element p : parents) { + int i = findIndex(p, item); + int t = findIndex(p, tgt); + if (i > -1 && t > -1) { + return i > t; + } + } + return false; // unsure... shouldn't ever get to this point; + } + + + private int findIndex(Element parent, Element descendant) { + for (int i = 0; i < parent.getChildren().size(); i++) { + if (parent.getChildren().get(i) == descendant || isChild(parent.getChildren().get(i), descendant)) + return i; + } + return -1; + } + + private boolean isChild(Element element, Element descendant) { + for (Element e : element.getChildren()) { + if (e == descendant) + return true; + if (isChild(e, descendant)) return true; } + return false; + } - private void validateBundle(List errors, Element bundle, NodeStack stack, boolean checkSpecials) { - List entries = new ArrayList(); - bundle.getNamedChildren("entry", entries); - String type = bundle.getNamedChildValue("type"); - type = StringUtils.defaultString(type); + private Element getQuestionById(Element focus, String ql) { + List list = getItems(focus); + for (Element item : list) { + String v = item.getNamedChildValue("linkId"); + if (ql.equals(v)) + return item; + Element tgt = getQuestionById(item, ql); + if (tgt != null) + return tgt; + } + return null; - if (entries.size() == 0) { - rule(errors, IssueType.INVALID, stack.getLiteralPath(), !(type.equals("document") || type.equals("message")), "Documents or Messages must contain at least one entry"); + } + + private List getItems(Element element) { + List list = new ArrayList<>(); + element.getNamedChildren("item", list); + return list; + } + + private void checkLang(Element resource, NodeStack stack) { + String lang = resource.getNamedChildValue("language"); + if (!Utilities.noString(lang)) + stack.workingLang = lang; + } + + private void validateResourceRules(List errors, Element element, NodeStack stack) { + String lang = element.getNamedChildValue("language"); + Element text = element.getNamedChild("text"); + if (text != null) { + Element div = text.getNamedChild("div"); + if (lang != null && div != null) { + XhtmlNode xhtml = div.getXhtml(); + String l = xhtml.getAttribute("lang"); + String xl = xhtml.getAttribute("xml:lang"); + if (l == null && xl == null) { + warning(errors, IssueType.BUSINESSRULE, div.line(), div.col(), stack.getLiteralPath(), false,messages.getString("Resource_has_a_language_but_the_XHTML_does_not_have_an_lang_or_an_xmllang_tag_needs_both__see_httpswwww3orgTRi18nhtmltechlanglangvalues")); } 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("fullUrl"); - - 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", ":0"), resource != null, "No resource on first entry")) { - validateDocument(errors, entries, resource, firstStack.push(resource, -1, null, null), fullUrl, id); - } - 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", ":0"), resource != null, "No resource on first entry")) { - 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); + if (l == null) { + warning(errors, IssueType.BUSINESSRULE, div.line(), div.col(), stack.getLiteralPath(), false,messages.getString("Resource_has_a_language_but_the_XHTML_does_not_have_a_lang_tag_needs_both_lang_and_xmllang__see_httpswwww3orgTRi18nhtmltechlanglangvalues")); + } else if (!l.equals(lang)) { + warning(errors, IssueType.BUSINESSRULE, div.line(), div.col(), stack.getLiteralPath(), false,messages.getString("Resource_has_a_language__and_the_XHTML_has_a_lang__but_they_differ_"), lang, l); + } + if (xl == null) { + warning(errors, IssueType.BUSINESSRULE, div.line(), div.col(), stack.getLiteralPath(), false,messages.getString("Resource_has_a_language_but_the_XHTML_does_not_have_an_xmllang_tag_needs_both_lang_and_xmllang__see_httpswwww3orgTRi18nhtmltechlanglangvalues")); + } else if (!xl.equals(lang)) { + warning(errors, IssueType.BUSINESSRULE, div.line(), div.col(), stack.getLiteralPath(), false,messages.getString("Resource_has_a_language__and_the_XHTML_has_an_xmllang__but_they_differ_"), lang, xl); + } } - for (Element entry : entries) { - String fullUrl = entry.getNamedChildValue("fullUrl"); - 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", ":0"), false, "The canonical URL (" + url + ") cannot match the fullUrl (" + fullUrl + ") unless the resource id (" + id + ") also matches"); - rule(errors, IssueType.INVALID, entry.line(), entry.col(), stack.addToLiteralPath("entry", ":0"), !url.equals(fullUrl) || serverBase == null || (url.equals(Utilities.pathURL(serverBase, entry.getNamedChild("resource").fhirType(), id))), "The canonical URL (" + url + ") cannot match the fullUrl (" + fullUrl + ") unless on the canonical server itself"); + } + } + // security tags are a set (system|code) + Element meta = element.getNamedChild("meta"); + if (meta != null) { + Set tags = new HashSet<>(); + List list = new ArrayList<>(); + meta.getNamedChildren("security", list); + int i = 0; + for (Element e : list) { + String s = e.getNamedChildValue("system") + "#" + e.getNamedChildValue("code"); + rule(errors, IssueType.BUSINESSRULE, e.line(), e.col(), stack.getLiteralPath() + ".meta.profile[" + Integer.toString(i) + "]", !tags.contains(s),messages.getString("Duplicate_Security_Label_"), s); + tags.add(s); + i++; + } + } + } + + private void validateCapabilityStatement(List errors, Element cs, NodeStack stack) { + int iRest = 0; + for (Element rest : cs.getChildrenByName("rest")) { + int iResource = 0; + for (Element resource : rest.getChildrenByName("resource")) { + int iSP = 0; + for (Element searchParam : resource.getChildrenByName("searchParam")) { + String ref = searchParam.getChildValue("definition"); + String type = searchParam.getChildValue("type"); + if (!Utilities.noString(ref)) { + SearchParameter sp = context.fetchResource(SearchParameter.class, ref); + if (sp != null) { + rule(errors, IssueType.INVALID, searchParam.line(), searchParam.col(), stack.literalPath + ".rest[" + iRest + "].resource[" + iResource + "].searchParam[" + iSP + "]", sp.getType().toCode().equals(type),messages.getString("Type_mismatch__SearchParameter__type_is__but_type_here_is_"), sp.getUrl(), sp.getType().toCode(), type); } - // todo: check specials + } + iSP++; } + iResource++; + } + iRest++; } + } - // 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 errors, List entries, NodeStack stack) { - // TODO: Need to handle _version - int i = 1; - for (Element entry : entries) { - String fullUrl = entry.getNamedChildValue("fullUrl"); - 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), - "Resource ID does not match the ID in the entry full URL ('" + id + "' vs '" + fullUrl + "') "); + private void validateCodeSystem(List errors, Element cs, NodeStack stack) { + String url = cs.getNamedChildValue("url"); + String vsu = cs.getNamedChildValue("valueSet"); + if (!Utilities.noString(vsu)) { + ValueSet vs; + try { + vs = context.fetchResourceWithException(ValueSet.class, vsu); + } catch (FHIRException e) { + vs = null; + } + if (vs != null) { + if (rule(errors, IssueType.BUSINESSRULE, stack.getLiteralPath(), vs.hasCompose() && !vs.hasExpansion(),messages.getString("CodeSystem__has_a_all_system_value_set_of__but_it_is_an_expansion"), url, vsu)) + if (rule(errors, IssueType.BUSINESSRULE, stack.getLiteralPath(), vs.getCompose().getInclude().size() == 1,messages.getString("CodeSystem__has_a_all_system_value_set_of__but_doesnt_have_a_single_include"), url, vsu)) + if (rule(errors, IssueType.BUSINESSRULE, stack.getLiteralPath(), vs.getCompose().getInclude().get(0).getSystem().equals(url),messages.getString("CodeSystem__has_a_all_system_value_set_of__but_doesnt_have_a_matching_system_"), url, vsu, vs.getCompose().getInclude().get(0).getSystem())) { + rule(errors, IssueType.BUSINESSRULE, stack.getLiteralPath(), !vs.getCompose().getInclude().get(0).hasValueSet() && !vs.getCompose().getInclude().get(0).hasConcept() && !vs.getCompose().getInclude().get(0).hasFilter(),messages.getString("CodeSystem__has_a_all_system_value_set_of__but_the_include_has_extra_details"), url, vsu); } - i++; + } + } // todo... try getting the value set the other way... + } + + private void validateQuestionannaireResponse(ValidatorHostContext hostContext, List errors, Element element, NodeStack stack) throws FHIRException { + Element q = element.getNamedChild("questionnaire"); + String questionnaire = null; + if (q != null) { + /* + * q.getValue() is correct for R4 content, but we'll also accept the second + * option just in case we're validating raw STU3 content. Being lenient here + * isn't the end of the world since if someone is actually doing the reference + * wrong in R4 content it'll get flagged elsewhere by the validator too + */ + if (isNotBlank(q.getValue())) { + questionnaire = q.getValue(); + } else if (isNotBlank(q.getChildValue("reference"))) { + questionnaire = q.getChildValue("reference"); + } + } + if (hint(errors, IssueType.REQUIRED, element.line(), element.col(), stack.getLiteralPath(), questionnaire != null,messages.getString("No_questionnaire_is_identified_so_no_validation_can_be_performed_against_the_base_questionnaire"))) { + long t = System.nanoTime(); + Questionnaire qsrc = questionnaire.startsWith("#") ? loadQuestionnaire(element, questionnaire.substring(1)) : context.fetchResource(Questionnaire.class, questionnaire); + sdTime = sdTime + (System.nanoTime() - t); + if (warning(errors, IssueType.REQUIRED, q.line(), q.col(), stack.getLiteralPath(), qsrc != null,messages.getString("The_questionnaire__could_not_be_resolved_so_no_validation_can_be_performed_against_the_base_questionnaire"), questionnaire)) { + boolean inProgress = "in-progress".equals(element.getNamedChildValue("status")); + validateQuestionannaireResponseItems(hostContext, qsrc, qsrc.getItem(), errors, element, stack, inProgress, element, new QStack(qsrc, element)); + } + } + } + + private Questionnaire loadQuestionnaire(Element resource, String id) throws FHIRException { + try { + for (Element contained : resource.getChildren("contained")) { + if (contained.getIdBase().equals(id)) { + FhirPublication v = FhirPublication.fromCode(context.getVersion()); + ByteArrayOutputStream bs = new ByteArrayOutputStream(); + new JsonParser(context).compose(contained, bs, OutputStyle.NORMAL, id); + byte[] json = bs.toByteArray(); + switch (v) { + case DSTU1: + throw new FHIRException("Unsupported version R1"); + case DSTU2: + org.hl7.fhir.dstu2.model.Resource r2 = new org.hl7.fhir.dstu2.formats.JsonParser().parse(json); + Resource r5 = VersionConvertor_10_50.convertResource(r2); + if (r5 instanceof Questionnaire) + return (Questionnaire) r5; + else + return null; + case DSTU2016May: + org.hl7.fhir.dstu2016may.model.Resource r2a = new org.hl7.fhir.dstu2016may.formats.JsonParser().parse(json); + r5 = VersionConvertor_14_50.convertResource(r2a); + if (r5 instanceof Questionnaire) + return (Questionnaire) r5; + else + return null; + case STU3: + org.hl7.fhir.dstu3.model.Resource r3 = new org.hl7.fhir.dstu3.formats.JsonParser().parse(json); + r5 = VersionConvertor_30_50.convertResource(r3, false); + if (r5 instanceof Questionnaire) + return (Questionnaire) r5; + else + return null; + case R4: + org.hl7.fhir.r4.model.Resource r4 = new org.hl7.fhir.r4.formats.JsonParser().parse(json); + r5 = VersionConvertor_40_50.convertResource(r4); + if (r5 instanceof Questionnaire) + return (Questionnaire) r5; + else + return null; + case R5: + r5 = new org.hl7.fhir.r5.formats.JsonParser().parse(json); + if (r5 instanceof Questionnaire) + return (Questionnaire) r5; + else + return null; + } } + } + return null; + } catch (IOException e) { + throw new FHIRException(e); + } + } + + private void validateQuestionnaireResponseItem(ValidatorHostContext hostContext, Questionnaire qsrc, QuestionnaireItemComponent qItem, List errors, Element element, NodeStack stack, boolean inProgress, Element questionnaireResponseRoot, QStack qstack) { + String text = element.getNamedChildValue("text"); + rule(errors, IssueType.INVALID, element.line(), element.col(), stack.getLiteralPath(), Utilities.noString(text) || text.equals(qItem.getText()),messages.getString("If_text_exists_it_must_match_the_questionnaire_definition_for_linkId_"), qItem.getLinkId()); + + List answers = new ArrayList(); + element.getNamedChildren("answer", answers); + if (inProgress) + warning(errors, IssueType.REQUIRED, element.line(), element.col(), stack.getLiteralPath(), isAnswerRequirementFulfilled(qItem, answers),messages.getString("No_response_answer_found_for_required_item_"), qItem.getLinkId()); + else if (myEnableWhenEvaluator.isQuestionEnabled(hostContext, qItem, qstack, fpe)) { + rule(errors, IssueType.REQUIRED, element.line(), element.col(), stack.getLiteralPath(), isAnswerRequirementFulfilled(qItem, answers),messages.getString("No_response_answer_found_for_required_item_"), qItem.getLinkId()); + } else if (!answers.isEmpty()) { // items without answers should be allowed, but not items with answers to questions that are disabled + // it appears that this is always a duplicate error - it will always already have beeb reported, so no need to report it again? + // GDG 2019-07-13 +// rule(errors, IssueType.INVALID, element.line(), element.col(), stack.getLiteralPath(), !isAnswerRequirementFulfilled(qItem, answers),messages.getString("Item_has_answer_2_even_though_it_is_not_enabled_"), qItem.getLinkId()); } - private void checkAllInterlinked(List errors, List entries, NodeStack stack, Element bundle, boolean isError) { - List 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 references = findReferences(e.getEntry()); - for (String ref : references) { - Element tgt = resolveInBundle(entries, ref, e.getEntry().getChildValue("fullUrl"), e.getResource().fhirType(), e.getResource().getIdBase()); - if (tgt != null) { - EntrySummary t = entryForTarget(entryList, tgt); - if (t != null) { - e.getTargets().add(t); - } - } - } - } + if (answers.size() > 1) + rule(errors, IssueType.INVALID, answers.get(1).line(), answers.get(1).col(), stack.getLiteralPath(), qItem.getRepeats(),messages.getString("Only_one_response_answer_item_with_this_linkId_allowed")); - Set 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); - } - } + for (Element answer : answers) { + NodeStack ns = stack.push(answer, -1, null, null); + if (qItem.getType() != null) { + switch (qItem.getType()) { + case GROUP: + rule(errors, IssueType.STRUCTURE, answer.line(), answer.col(), stack.getLiteralPath(), false,messages.getString("Items_of_type_group_should_not_have_answers")); + break; + case DISPLAY: // nothing + break; + case BOOLEAN: + validateQuestionnaireResponseItemType(errors, answer, ns, "boolean"); + break; + case DECIMAL: + validateQuestionnaireResponseItemType(errors, answer, ns, "decimal"); + break; + case INTEGER: + validateQuestionnaireResponseItemType(errors, answer, ns, "integer"); + break; + case DATE: + validateQuestionnaireResponseItemType(errors, answer, ns, "date"); + break; + case DATETIME: + validateQuestionnaireResponseItemType(errors, answer, ns, "dateTime"); + break; + case TIME: + validateQuestionnaireResponseItemType(errors, answer, ns, "time"); + break; + case STRING: + validateQuestionnaireResponseItemType(errors, answer, ns, "string"); + break; + case TEXT: + validateQuestionnaireResponseItemType(errors, answer, ns, "text"); + break; + case URL: + validateQuestionnaireResponseItemType(errors, answer, ns, "uri"); + break; + case ATTACHMENT: + validateQuestionnaireResponseItemType(errors, answer, ns, "Attachment"); + break; + case REFERENCE: + validateQuestionnaireResponseItemType(errors, answer, ns, "Reference"); + break; + case QUANTITY: + if ("Quantity".equals(validateQuestionnaireResponseItemType(errors, answer, ns, "Quantity"))) + if (qItem.hasExtension("???")) + validateQuestionnaireResponseItemQuantity(errors, answer, ns); + break; + case CHOICE: + String itemType = validateQuestionnaireResponseItemType(errors, answer, ns, "Coding", "date", "time", "integer", "string"); + if (itemType != null) { + if (itemType.equals("Coding")) validateAnswerCode(errors, answer, ns, qsrc, qItem, false); + else if (itemType.equals("date")) checkOption(errors, answer, ns, qsrc, qItem, "date"); + else if (itemType.equals("time")) checkOption(errors, answer, ns, qsrc, qItem, "time"); + else if (itemType.equals("integer")) + checkOption(errors, answer, ns, qsrc, qItem, "integer"); + else if (itemType.equals("string")) checkOption(errors, answer, ns, qsrc, qItem, "string"); } - } while (foundRevLinks); + break; + case OPENCHOICE: + itemType = validateQuestionnaireResponseItemType(errors, answer, ns, "Coding", "date", "time", "integer", "string"); + if (itemType != null) { + if (itemType.equals("Coding")) validateAnswerCode(errors, answer, ns, qsrc, qItem, true); + else if (itemType.equals("date")) checkOption(errors, answer, ns, qsrc, qItem, "date"); + else if (itemType.equals("time")) checkOption(errors, answer, ns, qsrc, qItem, "time"); + else if (itemType.equals("integer")) + checkOption(errors, answer, ns, qsrc, qItem, "integer"); + else if (itemType.equals("string")) + checkOption(errors, answer, ns, qsrc, qItem, "string", true); + } + break; +// case QUESTION: + case NULL: + // no validation + break; + } + } + validateQuestionannaireResponseItems(hostContext, qsrc, qItem.getItem(), errors, answer, stack, inProgress, questionnaireResponseRoot, qstack); + } + if (qItem.getType() == null) { + fail(errors, IssueType.REQUIRED, element.line(), element.col(), stack.getLiteralPath(), false,messages.getString("Definition_for_item__does_not_contain_a_type"), qItem.getLinkId()); + } else if (qItem.getType() == QuestionnaireItemType.DISPLAY) { + List items = new ArrayList(); + element.getNamedChildren("item", items); + rule(errors, IssueType.STRUCTURE, element.line(), element.col(), stack.getLiteralPath(), items.isEmpty(),messages.getString("Items_not_of_type_DISPLAY_should_not_have_items__linkId_0"), qItem.getLinkId()); + } else { + validateQuestionannaireResponseItems(hostContext, qsrc, qItem.getItem(), errors, element, stack, inProgress, questionnaireResponseRoot, qstack); + } + } + private boolean isAnswerRequirementFulfilled(QuestionnaireItemComponent qItem, List answers) { + return !answers.isEmpty() || !qItem.getRequired() || qItem.getType() == QuestionnaireItemType.GROUP; + } + + private void validateQuestionnaireResponseItem(ValidatorHostContext hostcontext, Questionnaire qsrc, QuestionnaireItemComponent qItem, List errors, List elements, NodeStack stack, boolean inProgress, Element questionnaireResponseRoot, QStack qstack) { + if (elements.size() > 1) + rule(errors, IssueType.INVALID, elements.get(1).line(), elements.get(1).col(), stack.getLiteralPath(), qItem.getRepeats(),messages.getString("Only_one_response_item_with_this_linkId_allowed__"), qItem.getLinkId()); + int i = 0; + for (Element element : elements) { + NodeStack ns = stack.push(element, i, null, null); + validateQuestionnaireResponseItem(hostcontext, qsrc, qItem, errors, element, ns, inProgress, questionnaireResponseRoot, qstack.push(qItem, element)); + i++; + } + } + + private int getLinkIdIndex(List qItems, String linkId) { + for (int i = 0; i < qItems.size(); i++) { + if (linkId.equals(qItems.get(i).getLinkId())) + return i; + } + return -1; + } + + private void validateQuestionannaireResponseItems(ValidatorHostContext hostContext, Questionnaire qsrc, List qItems, List errors, Element element, NodeStack stack, boolean inProgress, Element questionnaireResponseRoot, QStack qstack) { + List items = new ArrayList(); + element.getNamedChildren("item", items); + // now, sort into stacks + Map> map = new HashMap>(); + int lastIndex = -1; + for (Element item : items) { + String linkId = item.getNamedChildValue("linkId"); + if (rule(errors, IssueType.REQUIRED, item.line(), item.col(), stack.getLiteralPath(), !Utilities.noString(linkId),messages.getString("No_LinkId_so_cant_be_validated"))) { + int index = getLinkIdIndex(qItems, linkId); + if (index == -1) { + QuestionnaireItemComponent qItem = findQuestionnaireItem(qsrc, linkId); + if (qItem != null) { + rule(errors, IssueType.STRUCTURE, item.line(), item.col(), stack.getLiteralPath(), index > -1, misplacedItemError(qItem)); + NodeStack ns = stack.push(item, -1, null, null); + validateQuestionnaireResponseItem(hostContext, qsrc, qItem, errors, item, ns, inProgress, questionnaireResponseRoot, qstack.push(qItem, item)); + } else + rule(errors, IssueType.NOTFOUND, item.line(), item.col(), stack.getLiteralPath(), index > -1,messages.getString("LinkId__not_found_in_questionnaire"), linkId); + } else { + rule(errors, IssueType.STRUCTURE, item.line(), item.col(), stack.getLiteralPath(), index >= lastIndex,messages.getString("Structural_Error_items_are_out_of_order")); + lastIndex = index; + + // If an item has a child called "linkId" but no child called "answer", + // we'll treat it as not existing for the purposes of enableWhen validation + if (item.hasChildren("answer") || item.hasChildren("item")) { + List mapItem = map.computeIfAbsent(linkId, key -> new ArrayList<>()); + mapItem.add(item); + } + } + } + } + + // ok, now we have a list of known items, grouped by linkId. We've made an error for anything out of order + for (QuestionnaireItemComponent qItem : qItems) { + List mapItem = map.get(qItem.getLinkId()); + validateQuestionnaireResponseItem(hostContext, qsrc, errors, element, stack, inProgress, questionnaireResponseRoot, qItem, mapItem, qstack); + } + } + + public void validateQuestionnaireResponseItem(ValidatorHostContext hostContext, Questionnaire qsrc, List errors, Element element, NodeStack stack, boolean inProgress, Element questionnaireResponseRoot, QuestionnaireItemComponent qItem, List mapItem, QStack qstack) { + boolean enabled = myEnableWhenEvaluator.isQuestionEnabled(hostContext, qItem, qstack, fpe); + if (mapItem != null) { + if (!enabled) { 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), "Entry " + (entry.getChildValue("fullUrl") != null ? "'" + entry.getChildValue("fullUrl") + "'" : "") + " isn't reachable by traversing from first Bundle entry"); - } else { - warning(errors, IssueType.INFORMATIONAL, entry.line(), entry.col(), stack.addToLiteralPath("entry" + '[' + (i + 1) + ']'), visited.contains(e), "Entry " + (entry.getChildValue("fullUrl") != null ? "'" + entry.getChildValue("fullUrl") + "'" : "") + " isn't reachable by traversing from first Bundle entry"); - } - i++; + for (Element e : mapItem) { + NodeStack ns = stack.push(e, i, e.getProperty().getDefinition(), e.getProperty().getDefinition()); + rule(errors, IssueType.INVALID, e.line(), e.col(), ns.getLiteralPath(), enabled,messages.getString("Item_has_answer_even_though_it_is_not_enabled_item_id__"), qItem.getLinkId()); + i++; } + } + + // Recursively validate child items + validateQuestionnaireResponseItem(hostContext, qsrc, qItem, errors, mapItem, stack, inProgress, questionnaireResponseRoot, qstack); + + } else { + + // item is missing, is the question enabled? + if (enabled && qItem.getRequired()) { + String message = "No response found for required item with id = '" + qItem.getLinkId() + "'"; + if (inProgress) { + warning(errors, IssueType.REQUIRED, element.line(), element.col(), stack.getLiteralPath(), false, message); + } else { + rule(errors, IssueType.REQUIRED, element.line(), element.col(), stack.getLiteralPath(), false, message); + } + } + } - private EntrySummary entryForTarget(List entryList, Element tgt) { - for (EntrySummary e : entryList) { - if (e.getEntry() == tgt) { - return e; - } + } + + private String misplacedItemError(QuestionnaireItemComponent qItem) { + return qItem.hasLinkId() ? String.format("Structural Error: item with linkid %s is in the wrong place", qItem.getLinkId()) : "Structural Error: item is in the wrong place"; + } + + private void validateQuestionnaireResponseItemQuantity(List errors, Element answer, NodeStack stack) { + + } + + private String validateQuestionnaireResponseItemType(List errors, Element element, NodeStack stack, String... types) { + List values = new ArrayList(); + element.getNamedChildrenWithWildcard("value[x]", values); + for (int i = 0; i < types.length; i++) { + if (types[i].equals("text")) { + types[i] = "string"; + } + } + if (values.size() > 0) { + NodeStack ns = stack.push(values.get(0), -1, null, null); + CommaSeparatedStringBuilder l = new CommaSeparatedStringBuilder(); + for (String s : types) { + l.append(s); + if (values.get(0).getName().equals("value" + Utilities.capitalize(s))) + return (s); + } + if (types.length == 1) + rule(errors, IssueType.STRUCTURE, values.get(0).line(), values.get(0).col(), ns.getLiteralPath(), false,messages.getString("Answer_value_must_be_of_type_"), types[0]); + else + rule(errors, IssueType.STRUCTURE, values.get(0).line(), values.get(0).col(), ns.getLiteralPath(), false,messages.getString("Answer_value_must_be_one_of_the_types_"), l.toString()); + } + return null; + } + + private QuestionnaireItemComponent findQuestionnaireItem(Questionnaire qSrc, String linkId) { + return findItem(qSrc.getItem(), linkId); + } + + private QuestionnaireItemComponent findItem(List list, String linkId) { + for (QuestionnaireItemComponent item : list) { + if (linkId.equals(item.getLinkId())) + return item; + QuestionnaireItemComponent result = findItem(item.getItem(), linkId); + if (result != null) + return result; + } + return null; + } + + private void validateAnswerCode(List errors, Element value, NodeStack stack, Questionnaire qSrc, String ref, boolean theOpenChoice) { + ValueSet vs = resolveBindingReference(qSrc, ref, qSrc.getUrl()); + if (warning(errors, IssueType.CODEINVALID, value.line(), value.col(), stack.getLiteralPath(), vs != null,messages.getString("ValueSet__not_found_by_validator"), describeReference(ref))) { + try { + Coding c = ObjectConverter.readAsCoding(value); + if (isBlank(c.getCode()) && isBlank(c.getSystem()) && isNotBlank(c.getDisplay())) { + if (theOpenChoice) { + return; + } } + + long t = System.nanoTime(); + ValidationResult res = context.validateCode(new ValidationOptions(stack.workingLang), c, vs); + txTime = txTime + (System.nanoTime() - t); + if (!res.isOk()) { + txRule(errors, res.getTxLink(), IssueType.CODEINVALID, value.line(), value.col(), stack.getLiteralPath(), false, "The value provided (" + c.getSystem() + "::" + c.getCode() + ") is not in the options value set in the questionnaire"); + } else if (res.getSeverity() != null) { + super.addValidationMessage(errors, IssueType.CODEINVALID, value.line(), value.col(), stack.getLiteralPath(), res.getMessage(), res.getSeverity(), Source.TerminologyEngine); + } + } catch (Exception e) { + warning(errors, IssueType.CODEINVALID, value.line(), value.col(), stack.getLiteralPath(), false,messages.getString("Error__validating_Coding_against_Questionnaire_Options"), e.getMessage()); + } + } + } + + private void validateAnswerCode(List errors, Element answer, NodeStack stack, Questionnaire qSrc, QuestionnaireItemComponent qItem, boolean theOpenChoice) { + Element v = answer.getNamedChild("valueCoding"); + NodeStack ns = stack.push(v, -1, null, null); + if (qItem.getAnswerOption().size() > 0) + checkCodingOption(errors, answer, stack, qSrc, qItem, theOpenChoice); + // validateAnswerCode(errors, v, stack, qItem.getOption()); + else if (qItem.hasAnswerValueSet()) + validateAnswerCode(errors, v, stack, qSrc, qItem.getAnswerValueSet(), theOpenChoice); + else + hint(errors, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), false,messages.getString("Cannot_validate_options_because_no_option_or_options_are_provided")); + } + + private void checkOption(List errors, Element answer, NodeStack stack, Questionnaire qSrc, QuestionnaireItemComponent qItem, String type) { + checkOption(errors, answer, stack, qSrc, qItem, type, false); + } + + private void checkOption(List errors, Element answer, NodeStack stack, Questionnaire qSrc, QuestionnaireItemComponent qItem, String type, boolean openChoice) { + if (type.equals("integer")) checkIntegerOption(errors, answer, stack, qSrc, qItem, openChoice); + else if (type.equals("date")) checkDateOption(errors, answer, stack, qSrc, qItem, openChoice); + else if (type.equals("time")) checkTimeOption(errors, answer, stack, qSrc, qItem, openChoice); + else if (type.equals("string")) checkStringOption(errors, answer, stack, qSrc, qItem, openChoice); + else if (type.equals("Coding")) checkCodingOption(errors, answer, stack, qSrc, qItem, openChoice); + } + + private void checkIntegerOption(List errors, Element answer, NodeStack stack, Questionnaire qSrc, QuestionnaireItemComponent qItem, boolean openChoice) { + Element v = answer.getNamedChild("valueInteger"); + NodeStack ns = stack.push(v, -1, null, null); + if (qItem.getAnswerOption().size() > 0) { + List list = new ArrayList(); + for (QuestionnaireItemAnswerOptionComponent components : qItem.getAnswerOption()) { + try { + list.add(components.getValueIntegerType()); + } catch (FHIRException e) { + // If it's the wrong type, just keep going + } + } + if (list.isEmpty() && !openChoice) { + rule(errors, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), false,messages.getString("Option_list_has_no_option_values_of_type_integer")); + } else { + boolean found = false; + for (IntegerType item : list) { + if (item.getValue() == Integer.parseInt(v.primitiveValue())) { + found = true; + break; + } + } + if (!found) { + rule(errors, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), found,messages.getString("The_integer__is_not_a_valid_option"), v.primitiveValue()); + } + } + } else + hint(errors, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), false,messages.getString("Cannot_validate_integer_answer_option_because_no_option_list_is_provided")); + } + + private void checkDateOption(List errors, Element answer, NodeStack stack, Questionnaire qSrc, QuestionnaireItemComponent qItem, boolean openChoice) { + Element v = answer.getNamedChild("valueDate"); + NodeStack ns = stack.push(v, -1, null, null); + if (qItem.getAnswerOption().size() > 0) { + List list = new ArrayList(); + for (QuestionnaireItemAnswerOptionComponent components : qItem.getAnswerOption()) { + try { + list.add(components.getValueDateType()); + } catch (FHIRException e) { + // If it's the wrong type, just keep going + } + } + if (list.isEmpty() && !openChoice) { + rule(errors, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), false,messages.getString("Option_list_has_no_option_values_of_type_date")); + } else { + boolean found = false; + for (DateType item : list) { + if (item.getValue().equals(v.primitiveValue())) { + found = true; + break; + } + } + if (!found) { + rule(errors, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), found,messages.getString("The_date__is_not_a_valid_option"), v.primitiveValue()); + } + } + } else + hint(errors, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), false,messages.getString("Cannot_validate_date_answer_option_because_no_option_list_is_provided")); + } + + private void checkTimeOption(List errors, Element answer, NodeStack stack, Questionnaire qSrc, QuestionnaireItemComponent qItem, boolean openChoice) { + Element v = answer.getNamedChild("valueTime"); + NodeStack ns = stack.push(v, -1, null, null); + if (qItem.getAnswerOption().size() > 0) { + List list = new ArrayList(); + for (QuestionnaireItemAnswerOptionComponent components : qItem.getAnswerOption()) { + try { + list.add(components.getValueTimeType()); + } catch (FHIRException e) { + // If it's the wrong type, just keep going + } + } + if (list.isEmpty() && !openChoice) { + rule(errors, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), false,messages.getString("Option_list_has_no_option_values_of_type_time")); + } else { + boolean found = false; + for (TimeType item : list) { + if (item.getValue().equals(v.primitiveValue())) { + found = true; + break; + } + } + if (!found) { + rule(errors, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), found,messages.getString("The_time__is_not_a_valid_option"), v.primitiveValue()); + } + } + } else + hint(errors, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), false,messages.getString("Cannot_validate_time_answer_option_because_no_option_list_is_provided")); + } + + private void checkStringOption(List errors, Element answer, NodeStack stack, Questionnaire qSrc, QuestionnaireItemComponent qItem, boolean openChoice) { + Element v = answer.getNamedChild("valueString"); + NodeStack ns = stack.push(v, -1, null, null); + if (qItem.getAnswerOption().size() > 0) { + List list = new ArrayList(); + for (QuestionnaireItemAnswerOptionComponent components : qItem.getAnswerOption()) { + try { + if (components.getValue() != null) { + list.add(components.getValueStringType()); + } + } catch (FHIRException e) { + // If it's the wrong type, just keep going + } + } + if (!openChoice) { + if (list.isEmpty()) { + rule(errors, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), false,messages.getString("Option_list_has_no_option_values_of_type_string")); + } else { + boolean found = false; + for (StringType item : list) { + if (item.getValue().equals((v.primitiveValue()))) { + found = true; + break; + } + } + if (!found) { + rule(errors, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), found,messages.getString("The_string__is_not_a_valid_option"), v.primitiveValue()); + } + } + } + } else { + hint(errors, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), false,messages.getString("Cannot_validate_string_answer_option_because_no_option_list_is_provided")); + } + } + + private void checkCodingOption(List errors, Element answer, NodeStack stack, Questionnaire qSrc, QuestionnaireItemComponent qItem, boolean openChoice) { + Element v = answer.getNamedChild("valueCoding"); + String system = v.getNamedChildValue("system"); + String code = v.getNamedChildValue("code"); + NodeStack ns = stack.push(v, -1, null, null); + if (qItem.getAnswerOption().size() > 0) { + List list = new ArrayList(); + for (QuestionnaireItemAnswerOptionComponent components : qItem.getAnswerOption()) { + try { + if (components.getValue() != null) { + list.add(components.getValueCoding()); + } + } catch (FHIRException e) { + // If it's the wrong type, just keep going + } + } + if (list.isEmpty() && !openChoice) { + rule(errors, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), false,messages.getString("Option_list_has_no_option_values_of_type_coding")); + } else { + boolean found = false; + for (Coding item : list) { + if (ObjectUtil.equals(item.getSystem(), system) && ObjectUtil.equals(item.getCode(), code)) { + found = true; + break; + } + } + if (!found) { + rule(errors, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), found,messages.getString("The_code__is_not_a_valid_option"), system, code); + } + } + } else + hint(errors, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), false,messages.getString("Cannot_validate_Coding_option_because_no_option_list_is_provided")); + } + + private String tail(String path) { + return path.substring(path.lastIndexOf(".") + 1); + } + + private String tryParse(String ref) { + String[] parts = ref.split("\\/"); + switch (parts.length) { + case 1: return null; + case 2: + return checkResourceType(parts[0]); + default: + if (parts[parts.length - 2].equals("_history")) + return checkResourceType(parts[parts.length - 4]); + else + return checkResourceType(parts[parts.length - 2]); } + } - private void visitLinked(Set visited, EntrySummary t) { - if (!visited.contains(t)) { - visited.add(t); - for (EntrySummary e : t.getTargets()) { - visitLinked(visited, e); - } + private boolean typesAreAllReference(List theType) { + for (TypeRefComponent typeRefComponent : theType) { + if (typeRefComponent.getCode().equals("Reference") == false) { + return false; + } + } + return true; + } + + private void validateBundle(List errors, Element bundle, NodeStack stack, boolean checkSpecials) { + List entries = new ArrayList(); + 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")),messages.getString("Documents_or_Messages_must_contain_at_least_one_entry")); + } 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("fullUrl"); + + 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", ":0"), resource != null,messages.getString("No_resource_on_first_entry"))) { + validateDocument(errors, entries, resource, firstStack.push(resource, -1, null, null), fullUrl, id); } - } - - private void followResourceLinks(Element entry, Map visitedResources, Map candidateEntries, List candidateResources, List errors, NodeStack stack) { - followResourceLinks(entry, visitedResources, candidateEntries, candidateResources, errors, stack, 0); - } - - private void followResourceLinks(Element entry, Map visitedResources, Map candidateEntries, List candidateResources, List errors, NodeStack stack, int depth) { - Element resource = entry.getNamedChild("resource"); - if (visitedResources.containsValue(resource)) - return; - - visitedResources.put(entry.getNamedChildValue("fullUrl"), resource); - - String type = null; - Set 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("fullUrl"), new ArrayList(), 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); - } + 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", ":0"), resource != null,messages.getString("No_resource_on_first_entry"))) { + 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("fullUrl"); + 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", ":0"), false,messages.getString("The_canonical_URL__cannot_match_the_fullUrl__unless_the_resource_id__also_matches"), url, fullUrl, id); + rule(errors, IssueType.INVALID, entry.line(), entry.col(), stack.addToLiteralPath("entry", ":0"), !url.equals(fullUrl) || serverBase == null || (url.equals(Utilities.pathURL(serverBase, entry.getNamedChild("resource").fhirType(), id))),messages.getString("The_canonical_URL__cannot_match_the_fullUrl__unless_on_the_canonical_server_itself"), url, fullUrl); + } + // todo: check specials + } + } + + // 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 errors, List entries, NodeStack stack) { + // TODO: Need to handle _version + int i = 1; + for (Element entry : entries) { + String fullUrl = entry.getNamedChildValue("fullUrl"); + 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),messages.getString("Resource_ID_does_not_match_the_ID_in_the_entry_full_URL__vs__"), id, fullUrl); + } + i++; + } + } + + private void checkAllInterlinked(List errors, List entries, NodeStack stack, Element bundle, boolean isError) { + List 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 references = findReferences(e.getEntry()); + for (String ref : references) { + Element tgt = resolveInBundle(entries, ref, e.getEntry().getChildValue("fullUrl"), e.getResource().fhirType(), e.getResource().getIdBase()); + if (tgt != null) { + EntrySummary t = entryForTarget(entryList, tgt); + if (t != null) { + e.getTargets().add(t); + } + } + } } - private Set findReferences(Element start) { - Set references = new HashSet(); - findReferences(start, references); - return references; + Set 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),messages.getString("Entry__isnt_reachable_by_traversing_from_first_Bundle_entry"), (entry.getChildValue("fullUrl") != null ? "'" + entry.getChildValue("fullUrl") + "'" : "")); + } else { + warning(errors, IssueType.INFORMATIONAL, entry.line(), entry.col(), stack.addToLiteralPath("entry" + '[' + (i + 1) + ']'), visited.contains(e),messages.getString("Entry__isnt_reachable_by_traversing_from_first_Bundle_entry"), (entry.getChildValue("fullUrl") != null ? "'" + entry.getChildValue("fullUrl") + "'" : "")); + } + i++; + } + } + + private EntrySummary entryForTarget(List entryList, Element tgt) { + for (EntrySummary e : entryList) { + if (e.getEntry() == tgt) { + return e; + } + } + return null; + } + + private void visitLinked(Set visited, EntrySummary t) { + if (!visited.contains(t)) { + visited.add(t); + for (EntrySummary e : t.getTargets()) { + visitLinked(visited, e); + } + } + } + + private void followResourceLinks(Element entry, Map visitedResources, Map candidateEntries, List candidateResources, List errors, NodeStack stack) { + followResourceLinks(entry, visitedResources, candidateEntries, candidateResources, errors, stack, 0); + } + + private void followResourceLinks(Element entry, Map visitedResources, Map candidateEntries, List candidateResources, List errors, NodeStack stack, int depth) { + Element resource = entry.getNamedChild("resource"); + if (visitedResources.containsValue(resource)) + return; + + visitedResources.put(entry.getNamedChildValue("fullUrl"), resource); + + String type = null; + Set 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("fullUrl"), new ArrayList(), 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 findReferences(Element start) { + Set references = new HashSet(); + findReferences(start, references); + return references; + } + + private void findReferences(Element start, Set 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 errors, List entries, Element ref, String name, NodeStack stack, String fullUrl, String type, String id) { + String reference = null; + try { + reference = ref.getNamedChildValue("reference"); + } catch (Error e) { + } - private void findReferences(Element start, Set 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); + if (ref != null && !Utilities.noString(reference)) { + Element target = resolveInBundle(entries, reference, fullUrl, type, id); + rule(errors, IssueType.INVALID, ref.line(), ref.col(), stack.addToLiteralPath("reference"), target != null,messages.getString("Cant_find__in_the_bundle_"), reference, name); + } + } + + private void validateContains(ValidatorHostContext hostContext, List errors, String path, ElementDefinition child, ElementDefinition context, Element resource, Element element, NodeStack stack, IdStatus idstatus) throws FHIRException { + String resourceName = element.getType(); + TypeRefComponent trr = null; + for (TypeRefComponent tr : child.getType()) { + if (tr.getCode().equals("Resource")) { + trr = tr; + break; + } + } + if (trr == null) { + rule(errors, IssueType.INFORMATIONAL, element.line(), element.col(), stack.getLiteralPath(), false,messages.getString("The_type__is_not_valid__no_resources_allowed_here"), resourceName); + } else if (isValidResourceType(resourceName, trr)) { + long t = System.nanoTime(); + StructureDefinition profile = this.context.fetchResource(StructureDefinition.class, "http://hl7.org/fhir/StructureDefinition/" + resourceName); + sdTime = sdTime + (System.nanoTime() - t); + // special case: resource wrapper is reset if we're crossing a bundle boundary, but not otherwise + ValidatorHostContext hc = null; + if (element.getSpecial() == SpecialElement.BUNDLE_ENTRY || element.getSpecial() == SpecialElement.BUNDLE_OUTCOME || element.getSpecial() == SpecialElement.PARAMETER) { + resource = element; + hc = hostContext.forEntry(element); + } else { + hc = hostContext.forContained(element); + } + trackUsage(profile, hostContext, element); + if (rule(errors, IssueType.INVALID, element.line(), element.col(), stack.getLiteralPath(), profile != null,messages.getString("No_profile_found_for_contained_resource_of_type_"), resourceName)) { + validateResource(hc, errors, resource, element, profile, idstatus, stack); + } + } else { + List types = new ArrayList<>(); + for (UriType u : trr.getProfile()) { + StructureDefinition sd = this.context.fetchResource(StructureDefinition.class, u.getValue()); + if (sd != null && !types.contains(sd.getType())) { + types.add(sd.getType()); } + } + if (types.size() == 1) { + rule(errors, IssueType.INFORMATIONAL, element.line(), element.col(), stack.getLiteralPath(), false,messages.getString("The_type__is_not_valid__must_be_"), resourceName, types.get(0)); + } else { + rule(errors, IssueType.INFORMATIONAL, element.line(), element.col(), stack.getLiteralPath(), false,messages.getString("The_type__is_not_valid__must_be_one_of_"), resourceName, types); + } + } + } + + private boolean isValidResourceType(String type, TypeRefComponent def) { + if (!def.hasProfile()) { + return true; + } + List list = new ArrayList<>(); + for (UriType u : def.getProfile()) { + StructureDefinition sdt = context.fetchResource(StructureDefinition.class, u.getValue()); + if (sdt != null) { + list.add(sdt); + } } - private void validateBundleReference(List errors, List 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)) { - Element target = resolveInBundle(entries, reference, fullUrl, type, id); - rule(errors, IssueType.INVALID, ref.line(), ref.col(), stack.addToLiteralPath("reference"), target != null, "Can't find '" + reference + "' in the bundle (" + name + ")"); - } - } - - private void validateContains(ValidatorHostContext hostContext, List errors, String path, ElementDefinition child, ElementDefinition context, Element resource, Element element, NodeStack stack, IdStatus idstatus) throws FHIRException { - String resourceName = element.getType(); - TypeRefComponent trr = null; - for (TypeRefComponent tr : child.getType()) { - if (tr.getCode().equals("Resource")) { - trr = tr; - break; - } - } - if (trr == null) { - rule(errors, IssueType.INFORMATIONAL, element.line(), element.col(), stack.getLiteralPath(), false, "The type '" + resourceName + " is not valid - no resources allowed here"); - } else if (isValidResourceType(resourceName, trr)) { - long t = System.nanoTime(); - StructureDefinition profile = this.context.fetchResource(StructureDefinition.class, "http://hl7.org/fhir/StructureDefinition/" + resourceName); - sdTime = sdTime + (System.nanoTime() - t); - // special case: resource wrapper is reset if we're crossing a bundle boundary, but not otherwise - ValidatorHostContext hc = null; - if (element.getSpecial() == SpecialElement.BUNDLE_ENTRY || element.getSpecial() == SpecialElement.BUNDLE_OUTCOME || element.getSpecial() == SpecialElement.PARAMETER) { - resource = element; - hc = hostContext.forEntry(element); - } else { - hc = hostContext.forContained(element); - } - trackUsage(profile, hostContext, element); - if (rule(errors, IssueType.INVALID, element.line(), element.col(), stack.getLiteralPath(), profile != null, "No profile found for contained resource of type '" + resourceName + "'")) { - validateResource(hc, errors, resource, element, profile, idstatus, stack); - } - } else { - List types = new ArrayList<>(); - for (UriType u : trr.getProfile()) { - StructureDefinition sd = this.context.fetchResource(StructureDefinition.class, u.getValue()); - if (sd != null && !types.contains(sd.getType())) { - types.add(sd.getType()); - } - } - if (types.size() == 1) { - rule(errors, IssueType.INFORMATIONAL, element.line(), element.col(), stack.getLiteralPath(), false, "The type '" + resourceName + "' is not valid - must be " + types.get(0)); - } else { - rule(errors, IssueType.INFORMATIONAL, element.line(), element.col(), stack.getLiteralPath(), false, "The type '" + resourceName + "' is not valid - must be one of " + types); - } - } - } - - private boolean isValidResourceType(String type, TypeRefComponent def) { - if (!def.hasProfile()) { + StructureDefinition sdt = context.fetchTypeDefinition(type); + while (sdt != null) { + if (def.getWorkingCode().equals("Resource")) { + for (StructureDefinition sd : list) { + if (sd.getUrl().equals(sdt.getUrl())) { return true; + } + if (sd.getType().equals(sdt.getType())) { + return true; + } } - List list = new ArrayList<>(); - for (UriType u : def.getProfile()) { - StructureDefinition sdt = context.fetchResource(StructureDefinition.class, u.getValue()); - if (sdt != null) { - list.add(sdt); - } - } + } + sdt = context.fetchResource(StructureDefinition.class, sdt.getBaseDefinition()); + } + return false; + } - StructureDefinition sdt = context.fetchTypeDefinition(type); - while (sdt != null) { - if (def.getWorkingCode().equals("Resource")) { - for (StructureDefinition sd : list) { - if (sd.getUrl().equals(sdt.getUrl())) { - return true; - } - if (sd.getType().equals(sdt.getType())) { - return true; - } - } - } - sdt = context.fetchResource(StructureDefinition.class, sdt.getBaseDefinition()); - } - return false; + private void validateDocument(List errors, List 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"),messages.getString("The_first_entry_in_a_document_must_be_a_composition"))) { + + // 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 errors, List entries, Element composition, NodeStack stack, String fullUrl, String id, String title, String parent, boolean repeats, String propName) { + List 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 errors, List entries, Element composition, NodeStack stack, String fullUrl, String id, boolean repeats, String propName, String title) { + if (repeats) { + List 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 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 { + + // check type invariants + checkInvariants(hostContext, errors, profile, definition, resource, element, stack, false); + if (definition.getFixed() != null) + checkFixedValue(errors, stack.getLiteralPath(), element, definition.getFixed(), profile.getUrl(), definition.getSliceName(), null); + + // get the list of direct defined children, including slices + List childDefinitions = ProfileUtilities.getChildMap(profile, definition); + if (childDefinitions.isEmpty()) { + if (actualType == null) + return; // there'll be an error elsewhere in this case, and we're going to stop. + childDefinitions = getActualTypeChildren(hostContext, element, actualType); + } else if (definition.getType().size() > 1) { + // this only happens when the profile constrains the abstract children but leaves th choice open. + if (actualType == null) + return; // there'll be an error elsewhere in this case, and we're going to stop. + List typeChildDefinitions = getActualTypeChildren(hostContext, element, actualType); + // what were going to do is merge them - the type is not allowed to constrain things that the child definitions already do (well, if it does, it'll be ignored) + mergeChildLists(childDefinitions, typeChildDefinitions, definition.getPath(), actualType); } - private void validateDocument(List errors, List 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"), - "The first entry in a document must be a composition")) { + List children = listChildren(element, stack); + List problematicPaths = assignChildren(hostContext, errors, profile, resource, stack, childDefinitions, children); - // 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"); + checkCardinalities(errors, profile, element, stack, childDefinitions, children, problematicPaths); + // 4. check order if any slices are ordered. (todo) - validateSections(errors, entries, composition, stack, fullUrl, id); + // 5. inspect each child for validity + for (ElementInfo ei : children) { + checkChild(hostContext, errors, profile, definition, resource, element, actualType, stack, inCodeableConcept, checkDisplayInContext, ei, extensionUrl); + } + } + + private void mergeChildLists(List master, List additional, String masterPath, String typePath) { + for (ElementDefinition ed : additional) { + boolean inMaster = false; + for (ElementDefinition t : master) { + String tp = masterPath + ed.getPath().substring(typePath.length()); + if (t.getPath().equals(tp)) { + inMaster = true; } + } + if (!inMaster) { + master.add(ed); + } } - public void validateDocumentSubReference(List errors, List entries, Element composition, NodeStack stack, String fullUrl, String id, String title, String parent, boolean repeats, String propName) { - List 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++; + + } + + // todo: the element definition in context might assign a constrained profile for the type? + public List getActualTypeChildren(ValidatorHostContext hostContext, Element element, String actualType) { + List childDefinitions; + StructureDefinition dt = null; + if (isAbsolute(actualType)) + dt = this.context.fetchResource(StructureDefinition.class, actualType); + else + dt = this.context.fetchResource(StructureDefinition.class, "http://hl7.org/fhir/StructureDefinition/" + actualType); + if (dt == null) + throw new DefinitionException("Unable to resolve actual type " + actualType); + trackUsage(dt, hostContext, element); + + childDefinitions = ProfileUtilities.getChildMap(dt, dt.getSnapshot().getElement().get(0)); + return childDefinitions; + } + + public void checkChild(ValidatorHostContext hostContext, List errors, StructureDefinition profile, ElementDefinition definition, + Element resource, Element element, String actualType, NodeStack stack, boolean inCodeableConcept, boolean checkDisplayInContext, ElementInfo ei, String extensionUrl) + throws FHIRException, DefinitionException { + + List profiles = new ArrayList(); + if (ei.definition != null) { + String type = null; + ElementDefinition typeDefn = null; + checkMustSupport(profile, ei); + + if (ei.definition.getType().size() == 1 && !"*".equals(ei.definition.getType().get(0).getWorkingCode()) && !"Element".equals(ei.definition.getType().get(0).getWorkingCode()) + && !"BackboneElement".equals(ei.definition.getType().get(0).getWorkingCode())) { + type = ei.definition.getType().get(0).getWorkingCode(); + // Excluding reference is a kludge to get around versioning issues + if (ei.definition.getType().get(0).hasProfile()) { + for (CanonicalType p : ei.definition.getType().get(0).getProfile()) { + profiles.add(p.getValue()); + } } - } - - public void validateDocumentReference(List errors, List entries, Element composition, NodeStack stack, String fullUrl, String id, boolean repeats, String propName, String title) { - if (repeats) { - List 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); - } + } else if (ei.definition.getType().size() == 1 && "*".equals(ei.definition.getType().get(0).getWorkingCode())) { + String prefix = tail(ei.definition.getPath()); + assert prefix.endsWith("[x]"); + type = ei.getName().substring(prefix.length() - 3); + if (isPrimitiveType(type)) + type = Utilities.uncapitalize(type); + if (ei.definition.getType().get(0).hasProfile()) { + for (CanonicalType p : ei.definition.getType().get(0).getProfile()) { + profiles.add(p.getValue()); + } } - } + } else if (ei.definition.getType().size() > 1) { - private void validateElement(ValidatorHostContext hostContext, List 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 { + String prefix = tail(ei.definition.getPath()); + assert typesAreAllReference(ei.definition.getType()) || ei.definition.hasRepresentation(PropertyRepresentation.TYPEATTR) || prefix.endsWith("[x]") : prefix; - // check type invariants - checkInvariants(hostContext, errors, profile, definition, resource, element, stack, false); - if (definition.getFixed() != null) - checkFixedValue(errors, stack.getLiteralPath(), element, definition.getFixed(), profile.getUrl(), definition.getSliceName(), null); - - // get the list of direct defined children, including slices - List childDefinitions = ProfileUtilities.getChildMap(profile, definition); - if (childDefinitions.isEmpty()) { - if (actualType == null) - return; // there'll be an error elsewhere in this case, and we're going to stop. - childDefinitions = getActualTypeChildren(hostContext, element, actualType); - } else if (definition.getType().size() > 1) { - // this only happens when the profile constrains the abstract children but leaves th choice open. - if (actualType == null) - return; // there'll be an error elsewhere in this case, and we're going to stop. - List typeChildDefinitions = getActualTypeChildren(hostContext, element, actualType); - // what were going to do is merge them - the type is not allowed to constrain things that the child definitions already do (well, if it does, it'll be ignored) - mergeChildLists(childDefinitions, typeChildDefinitions, definition.getPath(), actualType); - } - - List children = listChildren(element, stack); - List problematicPaths = assignChildren(hostContext, errors, profile, resource, stack, childDefinitions, children); - - checkCardinalities(errors, profile, element, stack, childDefinitions, children, problematicPaths); - // 4. check order if any slices are ordered. (todo) - - // 5. inspect each child for validity - for (ElementInfo ei : children) { - checkChild(hostContext, errors, profile, definition, resource, element, actualType, stack, inCodeableConcept, checkDisplayInContext, ei, extensionUrl); - } - } - - private void mergeChildLists(List master, List additional, String masterPath, String typePath) { - for (ElementDefinition ed : additional) { - boolean inMaster = false; - for (ElementDefinition t : master) { - String tp = masterPath + ed.getPath().substring(typePath.length()); - if (t.getPath().equals(tp)) { - inMaster = true; - } - } - if (!inMaster) { - master.add(ed); - } - } - - - } - - // todo: the element definition in context might assign a constrained profile for the type? - public List getActualTypeChildren(ValidatorHostContext hostContext, Element element, String actualType) { - List childDefinitions; - StructureDefinition dt = null; - if (isAbsolute(actualType)) - dt = this.context.fetchResource(StructureDefinition.class, actualType); - else - dt = this.context.fetchResource(StructureDefinition.class, "http://hl7.org/fhir/StructureDefinition/" + actualType); - if (dt == null) - throw new DefinitionException("Unable to resolve actual type " + actualType); - trackUsage(dt, hostContext, element); - - childDefinitions = ProfileUtilities.getChildMap(dt, dt.getSnapshot().getElement().get(0)); - return childDefinitions; - } - - public void checkChild(ValidatorHostContext hostContext, List errors, StructureDefinition profile, ElementDefinition definition, - Element resource, Element element, String actualType, NodeStack stack, boolean inCodeableConcept, boolean checkDisplayInContext, ElementInfo ei, String extensionUrl) - throws FHIRException, DefinitionException { - - List profiles = new ArrayList(); - if (ei.definition != null) { - String type = null; - ElementDefinition typeDefn = null; - checkMustSupport(profile, ei); - - if (ei.definition.getType().size() == 1 && !"*".equals(ei.definition.getType().get(0).getWorkingCode()) && !"Element".equals(ei.definition.getType().get(0).getWorkingCode()) - && !"BackboneElement".equals(ei.definition.getType().get(0).getWorkingCode())) { - type = ei.definition.getType().get(0).getWorkingCode(); - // Excluding reference is a kludge to get around versioning issues - if (ei.definition.getType().get(0).hasProfile()) { - for (CanonicalType p : ei.definition.getType().get(0).getProfile()) { - profiles.add(p.getValue()); - } - } - } else if (ei.definition.getType().size() == 1 && "*".equals(ei.definition.getType().get(0).getWorkingCode())) { - String prefix = tail(ei.definition.getPath()); - assert prefix.endsWith("[x]"); - type = ei.getName().substring(prefix.length() - 3); - if (isPrimitiveType(type)) - type = Utilities.uncapitalize(type); - if (ei.definition.getType().get(0).hasProfile()) { - for (CanonicalType p : ei.definition.getType().get(0).getProfile()) { - profiles.add(p.getValue()); - } - } - } else if (ei.definition.getType().size() > 1) { - - String prefix = tail(ei.definition.getPath()); - assert typesAreAllReference(ei.definition.getType()) || ei.definition.hasRepresentation(PropertyRepresentation.TYPEATTR) || prefix.endsWith("[x]") : prefix; - - if (ei.definition.hasRepresentation(PropertyRepresentation.TYPEATTR)) - type = ei.getElement().getType(); - else { - prefix = prefix.substring(0, prefix.length() - 3); - for (TypeRefComponent t : ei.definition.getType()) - if ((prefix + Utilities.capitalize(t.getWorkingCode())).equals(ei.getName())) { - type = t.getWorkingCode(); - // Excluding reference is a kludge to get around versioning issues - if (t.hasProfile() && !type.equals("Reference")) - profiles.add(t.getProfile().get(0).getValue()); - } - } - if (type == null) { - TypeRefComponent trc = ei.definition.getType().get(0); - if (trc.getWorkingCode().equals("Reference")) - type = "Reference"; - else - rule(errors, IssueType.STRUCTURE, ei.line(), ei.col(), stack.getLiteralPath(), false, - "The type of element " + ei.getName() + " is not known, which is illegal. Valid types at this point are " + describeTypes(ei.definition.getType())); - } - } else if (ei.definition.getContentReference() != null) { - typeDefn = resolveNameReference(profile.getSnapshot(), ei.definition.getContentReference()); - } else if (ei.definition.getType().size() == 1 && ("Element".equals(ei.definition.getType().get(0).getWorkingCode()) || "BackboneElement".equals(ei.definition.getType().get(0).getWorkingCode()))) { - if (ei.definition.getType().get(0).hasProfile()) { - CanonicalType pu = ei.definition.getType().get(0).getProfile().get(0); - if (pu.hasExtension(ToolingExtensions.EXT_PROFILE_ELEMENT)) - profiles.add(pu.getValue() + "#" + pu.getExtensionString(ToolingExtensions.EXT_PROFILE_ELEMENT)); - else - profiles.add(pu.getValue()); - } - } - - if (type != null) { - if (type.startsWith("@")) { - ei.definition = findElement(profile, type.substring(1)); - type = null; - } - } - NodeStack localStack = stack.push(ei.getElement(), ei.count, ei.definition, type == null ? typeDefn : resolveType(type, ei.definition.getType())); - if (debug) { - System.out.println(" " + localStack.getLiteralPath()); - } - String localStackLiterapPath = localStack.getLiteralPath(); - String eiPath = ei.getPath(); - assert (eiPath.equals(localStackLiterapPath)) : "ei.path: " + ei.getPath() + " - localStack.getLiteralPath: " + localStackLiterapPath; - boolean thisIsCodeableConcept = false; - String thisExtension = null; - boolean checkDisplay = true; - - checkInvariants(hostContext, errors, profile, ei.definition, resource, ei.getElement(), localStack, true); - - ei.getElement().markValidation(profile, ei.definition); - boolean elementValidated = false; - if (type != null) { - if (isPrimitiveType(type)) { - checkPrimitive(hostContext, errors, ei.getPath(), type, ei.definition, ei.getElement(), profile, stack); - } else { - if (ei.definition.hasFixed()) { - checkFixedValue(errors, ei.getPath(), ei.getElement(), ei.definition.getFixed(), profile.getUrl(), ei.definition.getSliceName(), null); - } - if (ei.definition.hasPattern()) { - checkFixedValue(errors, ei.getPath(), ei.getElement(), ei.definition.getPattern(), profile.getUrl(), ei.definition.getSliceName(), null, true); - } - } - if (type.equals("Identifier")) { - checkIdentifier(errors, ei.getPath(), ei.getElement(), ei.definition); - } else if (type.equals("Coding")) { - checkCoding(errors, ei.getPath(), ei.getElement(), profile, ei.definition, inCodeableConcept, checkDisplayInContext, stack); - } else if (type.equals("CodeableConcept")) { - checkDisplay = checkCodeableConcept(errors, ei.getPath(), ei.getElement(), profile, ei.definition, stack); - thisIsCodeableConcept = true; - } else if (type.equals("Reference")) { - checkReference(hostContext, errors, ei.getPath(), ei.getElement(), profile, ei.definition, actualType, localStack); - // We only check extensions if we're not in a complex extension or if the element we're dealing with is not defined as part of that complex extension - } else if (type.equals("Extension")) { - Element eurl = ei.getElement().getNamedChild("url"); - if (rule(errors, IssueType.INVALID, ei.getPath(), eurl != null, "Extension.url is required")) { - String url = eurl.primitiveValue(); - thisExtension = url; - if (rule(errors, IssueType.INVALID, ei.getPath(), !Utilities.noString(url), "Extension.url is required")) { - if (rule(errors, IssueType.INVALID, ei.getPath(), (extensionUrl != null) || Utilities.isAbsoluteUrl(url), "Extension.url must be an absolute URL")) { - checkExtension(hostContext, errors, ei.getPath(), resource, element, ei.getElement(), ei.definition, profile, localStack, stack, extensionUrl); - } - } - } - } else if (type.equals("Resource")) { - validateContains(hostContext, errors, ei.getPath(), ei.definition, definition, resource, ei.getElement(), localStack, idStatusForEntry(element, ei)); // if - elementValidated = true; - // (str.matches(".*([.,/])work\\1$")) - } else if (Utilities.isAbsoluteUrl(type)) { - StructureDefinition defn = context.fetchTypeDefinition(type); - if (defn != null && hasMapping("http://hl7.org/fhir/terminology-pattern", defn, defn.getSnapshot().getElementFirstRep())) { - List txtype = getMapping("http://hl7.org/fhir/terminology-pattern", defn, defn.getSnapshot().getElementFirstRep()); - if (txtype.contains("CodeableConcept")) { - checkTerminologyCodeableConcept(errors, ei.getPath(), ei.getElement(), profile, ei.definition, stack, defn); - thisIsCodeableConcept = true; - } else if (txtype.contains("Coding")) { - checkTerminologyCoding(errors, ei.getPath(), ei.getElement(), profile, ei.definition, inCodeableConcept, checkDisplayInContext, stack, defn); - } - } - } - } else { - if (rule(errors, IssueType.STRUCTURE, ei.line(), ei.col(), stack.getLiteralPath(), ei.definition != null, "Unrecognised Content " + ei.getName())) - validateElement(hostContext, errors, profile, ei.definition, null, null, resource, ei.getElement(), type, localStack, false, true, null); - } - StructureDefinition p = null; - String tail = null; - if (profiles.isEmpty()) { - if (type != null) { - p = getProfileForType(type, ei.definition.getType()); - - // If dealing with a primitive type, then we need to check the current child against - // the invariants (constraints) on the current element, because otherwise it only gets - // checked against the primary type's invariants: LLoyd - //if (p.getKind() == StructureDefinitionKind.PRIMITIVETYPE) { - // checkInvariants(hostContext, errors, ei.path, profile, ei.definition, null, null, resource, ei.element); - //} - - rule(errors, IssueType.STRUCTURE, ei.line(), ei.col(), ei.getPath(), p != null, "Unknown type " + type); - } - } else if (profiles.size() == 1) { - String url = profiles.get(0); - if (url.contains("#")) { - tail = url.substring(url.indexOf("#") + 1); - url = url.substring(0, url.indexOf("#")); - } - p = this.context.fetchResource(StructureDefinition.class, url); - rule(errors, IssueType.STRUCTURE, ei.line(), ei.col(), ei.getPath(), p != null, "Unknown profile " + profiles.get(0)); - } else { - elementValidated = true; - HashMap> goodProfiles = new HashMap>(); - HashMap> badProfiles = new HashMap>(); - for (String typeProfile : profiles) { - String url = typeProfile; - tail = null; - if (url.contains("#")) { - tail = url.substring(url.indexOf("#") + 1); - url = url.substring(0, url.indexOf("#")); - } - p = this.context.fetchResource(StructureDefinition.class, typeProfile); - if (rule(errors, IssueType.STRUCTURE, ei.line(), ei.col(), ei.getPath(), p != null, "Unknown profile " + typeProfile)) { - List profileErrors = new ArrayList(); - validateElement(hostContext, profileErrors, p, getElementByTail(p, tail), profile, ei.definition, resource, ei.getElement(), type, localStack, thisIsCodeableConcept, checkDisplay, thisExtension); - if (hasErrors(profileErrors)) - badProfiles.put(typeProfile, profileErrors); - else - goodProfiles.put(typeProfile, profileErrors); - } - } - if (goodProfiles.size() == 1) { - errors.addAll(goodProfiles.values().iterator().next()); - } else if (goodProfiles.size() == 0) { - rule(errors, IssueType.STRUCTURE, ei.line(), ei.col(), ei.getPath(), false, "Unable to find matching profile among choices: " + StringUtils.join("; ", profiles)); - for (String m : badProfiles.keySet()) { - p = this.context.fetchResource(StructureDefinition.class, m); - for (ValidationMessage message : badProfiles.get(m)) { - message.setMessage(message.getMessage() + " (validating against " + p.getUrl() + (p.hasVersion() ? "|" + p.getVersion() : "") + " [" + p.getName() + "])"); - errors.add(message); - } - } - } else { - warning(errors, IssueType.STRUCTURE, ei.line(), ei.col(), ei.getPath(), false, "Found multiple matching profiles among choices: " + StringUtils.join("; ", goodProfiles.keySet())); - for (String m : goodProfiles.keySet()) { - p = this.context.fetchResource(StructureDefinition.class, m); - for (ValidationMessage message : goodProfiles.get(m)) { - message.setMessage(message.getMessage() + " (validating against " + p.getUrl() + (p.hasVersion() ? "|" + p.getVersion() : "") + " [" + p.getName() + "])"); - errors.add(message); - } - } - } - } - if (p != null) { - trackUsage(p, hostContext, element); - - if (!elementValidated) { - if (ei.getElement().getSpecial() == SpecialElement.BUNDLE_ENTRY || ei.getElement().getSpecial() == SpecialElement.BUNDLE_OUTCOME || ei.getElement().getSpecial() == SpecialElement.PARAMETER) - validateElement(hostContext, errors, p, getElementByTail(p, tail), profile, ei.definition, ei.getElement(), ei.getElement(), type, localStack, thisIsCodeableConcept, checkDisplay, thisExtension); - else - validateElement(hostContext, errors, p, getElementByTail(p, tail), profile, ei.definition, resource, ei.getElement(), type, localStack, thisIsCodeableConcept, checkDisplay, thisExtension); - } - int index = profile.getSnapshot().getElement().indexOf(ei.definition); - if (index < profile.getSnapshot().getElement().size() - 1) { - String nextPath = profile.getSnapshot().getElement().get(index + 1).getPath(); - if (!nextPath.equals(ei.definition.getPath()) && nextPath.startsWith(ei.definition.getPath())) - validateElement(hostContext, errors, profile, ei.definition, null, null, resource, ei.getElement(), type, localStack, thisIsCodeableConcept, checkDisplay, thisExtension); - } - } - } - } - - private void trackUsage(StructureDefinition profile, ValidatorHostContext hostContext, Element element) { - if (tracker != null) { - tracker.recordProfileUsage(profile, hostContext.getAppContext(), element); - } - } - - private boolean hasMapping(String url, StructureDefinition defn, ElementDefinition elem) { - String id = null; - for (StructureDefinitionMappingComponent m : defn.getMapping()) { - if (url.equals(m.getUri())) { - id = m.getIdentity(); - break; - } - } - if (id != null) { - for (ElementDefinitionMappingComponent m : elem.getMapping()) { - if (id.equals(m.getIdentity())) { - return true; - } - } - - } - return false; - } - - private List getMapping(String url, StructureDefinition defn, ElementDefinition elem) { - List res = new ArrayList<>(); - String id = null; - for (StructureDefinitionMappingComponent m : defn.getMapping()) { - if (url.equals(m.getUri())) { - id = m.getIdentity(); - break; - } - } - if (id != null) { - for (ElementDefinitionMappingComponent m : elem.getMapping()) { - if (id.equals(m.getIdentity())) { - res.add(m.getMap()); - } - } - } - return res; - } - - public void checkMustSupport(StructureDefinition profile, ElementInfo ei) { - String usesMustSupport = profile.getUserString("usesMustSupport"); - if (usesMustSupport == null) { - usesMustSupport = "N"; - for (ElementDefinition pe : profile.getSnapshot().getElement()) { - if (pe.getMustSupport()) { - usesMustSupport = "Y"; - break; - } - } - profile.setUserData("usesMustSupport", usesMustSupport); - } - if (usesMustSupport.equals("Y")) { - String elementSupported = ei.getElement().getUserString("elementSupported"); - if (elementSupported == null || ei.definition.getMustSupport()) - if (ei.definition.getMustSupport()) { - ei.getElement().setUserData("elementSupported", "Y"); - } - } - } - - public void checkCardinalities(List errors, StructureDefinition profile, Element element, NodeStack stack, - List childDefinitions, List children, List problematicPaths) throws DefinitionException { - // 3. report any definitions that have a cardinality problem - for (ElementDefinition ed : childDefinitions) { - if (ed.getRepresentation().isEmpty()) { // ignore xml attributes - int count = 0; - List slices = null; - if (ed.hasSlicing()) - slices = ProfileUtilities.getSliceList(profile, ed); - for (ElementInfo ei : children) - if (ei.definition == ed) - count++; - else if (slices != null) { - for (ElementDefinition sed : slices) { - if (ei.definition == sed) { - count++; - break; - } - } - } - String location = "Profile " + profile.getUrl() + ", Element '" + stack.getLiteralPath() + "." + tail(ed.getPath()) + (ed.hasSliceName() ? "[" + ed.getSliceName() + (ed.hasLabel() ? " (" + ed.getLabel() + ")" : "") + "]" : "") + "'"; - if (ed.getMin() > 0) { - if (problematicPaths.contains(ed.getPath())) - hint(errors, IssueType.NOTSUPPORTED, element.line(), element.col(), stack.getLiteralPath(), count >= ed.getMin(), location + "': Unable to check minimum required (" + Integer.toString(ed.getMin()) + ") due to lack of slicing validation"); - else - rule(errors, IssueType.STRUCTURE, element.line(), element.col(), stack.getLiteralPath(), count >= ed.getMin(), location + ": minimum required = " + Integer.toString(ed.getMin()) + ", but only found " + Integer.toString(count)); - } - if (ed.hasMax() && !ed.getMax().equals("*")) { - if (problematicPaths.contains(ed.getPath())) - hint(errors, IssueType.NOTSUPPORTED, element.line(), element.col(), stack.getLiteralPath(), count <= Integer.parseInt(ed.getMax()), location + ": Unable to check max allowed (" + ed.getMax() + ") due to lack of slicing validation"); - else - rule(errors, IssueType.STRUCTURE, element.line(), element.col(), stack.getLiteralPath(), count <= Integer.parseInt(ed.getMax()), location + ": max allowed = " + ed.getMax() + ", but found " + Integer.toString(count)); - } - } - } - } - - public List assignChildren(ValidatorHostContext hostContext, List errors, StructureDefinition profile, Element resource, - NodeStack stack, List childDefinitions, List children) throws DefinitionException { - // 2. assign children to a definition - // for each definition, for each child, check whether it belongs in the slice - ElementDefinition slicer = null; - boolean unsupportedSlicing = false; - List problematicPaths = new ArrayList(); - String slicingPath = null; - int sliceOffset = 0; - for (int i = 0; i < childDefinitions.size(); i++) { - ElementDefinition ed = childDefinitions.get(i); - boolean childUnsupportedSlicing = false; - boolean process = true; - if (ed.hasSlicing() && !ed.getSlicing().getOrdered()) - slicingPath = ed.getPath(); - else if (slicingPath != null && ed.getPath().equals(slicingPath)) - ; // nothing - else if (slicingPath != null && !ed.getPath().startsWith(slicingPath)) - slicingPath = null; - // where are we with slicing - if (ed.hasSlicing()) { - if (slicer != null && slicer.getPath().equals(ed.getPath())) { - String errorContext = "profile " + profile.getUrl(); - if (!resource.getChildValue("id").isEmpty()) - errorContext += "; instance " + resource.getChildValue("id"); - throw new DefinitionException("Slice encountered midway through set (path = " + slicer.getPath() + ", id = " + slicer.getId() + "); " + errorContext); - } - slicer = ed; - process = false; - sliceOffset = i; - } else if (slicer != null && !slicer.getPath().equals(ed.getPath())) - slicer = null; - - for (ElementInfo ei : children) { - if (ei.sliceInfo == null) { - ei.sliceInfo = new ArrayList<>(); - } - unsupportedSlicing = matchSlice(hostContext, errors, ei.sliceInfo, profile, stack, slicer, unsupportedSlicing, problematicPaths, sliceOffset, i, ed, childUnsupportedSlicing, ei); - } - } - int last = -1; - int lastSlice = -1; - for (ElementInfo ei : children) { - String sliceInfo = ""; - if (slicer != null) - sliceInfo = " (slice: " + slicer.getPath() + ")"; - if (!unsupportedSlicing) - if (ei.additionalSlice && ei.definition != null) { - if (ei.definition.getSlicing().getRules().equals(ElementDefinition.SlicingRules.OPEN) || - ei.definition.getSlicing().getRules().equals(ElementDefinition.SlicingRules.OPENATEND) && true /* TODO: replace "true" with condition to check that this element is at "end" */) { - slicingHint(errors, IssueType.INFORMATIONAL, ei.line(), ei.col(), ei.getPath(), false, "This element does not match any known slice" + (profile == null ? "" : " defined in the profile " + profile.getUrl()), - "This element does not match any known slice" + (profile == null ? "" : " defined in the profile " + profile.getUrl() + ": " + errorSummaryForSlicingAsHtml(ei.sliceInfo))); - } else if (ei.definition.getSlicing().getRules().equals(ElementDefinition.SlicingRules.CLOSED)) { - rule(errors, IssueType.INVALID, ei.line(), ei.col(), ei.getPath(), false, "This element does not match any known slice " + (profile == null ? "" : " defined in the profile " + profile.getUrl() + " and slicing is CLOSED: " + errorSummaryForSlicing(ei.sliceInfo)), - "This element does not match any known slice " + (profile == null ? "" : " defined in the profile " + profile.getUrl() + " and slicing is CLOSED: " + errorSummaryForSlicingAsHtml(ei.sliceInfo))); - } - } else { - // Don't raise this if we're in an abstract profile, like Resource - if (!profile.getAbstract()) - rule(errors, IssueType.NOTSUPPORTED, ei.line(), ei.col(), ei.getPath(), (ei.definition != null), "This element is not allowed by the profile " + profile.getUrl()); - } - // TODO: Should get the order of elements correct when parsing elements that are XML attributes vs. elements - boolean isXmlAttr = false; - if (ei.definition != null) { - for (Enumeration r : ei.definition.getRepresentation()) { - if (r.getValue() == PropertyRepresentation.XMLATTR) { - isXmlAttr = true; - break; - } - } - } - - 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, "As specified by profile " + profile.getUrl() + ", Element '" + ei.getName() + "' is out of order"); - } - 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, "As specified by profile " + profile.getUrl() + ", Element '" + ei.getName() + "' is out of order in ordered slice"); - if (ei.definition == null || !isXmlAttr) - last = ei.index; - if (ei.slice != null) - lastSlice = ei.sliceindex; - else - lastSlice = -1; - } - return problematicPaths; - } - - public List listChildren(Element element, NodeStack stack) { - // 1. List the children, and remember their exact path (convenience) - List children = new ArrayList(); - ChildIterator iter = new ChildIterator(this, stack.getLiteralPath(), element); - while (iter.next()) - children.add(new ElementInfo(iter.name(), iter.element(), iter.path(), iter.count())); - return children; - } - - public void checkInvariants(ValidatorHostContext hostContext, List errors, StructureDefinition profile, ElementDefinition definition, Element resource, Element element, NodeStack stack, boolean onlyNonInherited) throws FHIRException { - checkInvariants(hostContext, errors, stack.getLiteralPath(), profile, definition, null, null, resource, element, onlyNonInherited); - } - - public boolean matchSlice(ValidatorHostContext hostContext, List errors, List sliceInfo, StructureDefinition profile, NodeStack stack, - ElementDefinition slicer, boolean unsupportedSlicing, List problematicPaths, int sliceOffset, int i, ElementDefinition ed, - boolean childUnsupportedSlicing, ElementInfo ei) { - boolean match = false; - if (slicer == null || slicer == ed) { - match = nameMatches(ei.getName(), tail(ed.getPath())); - } else { - if (nameMatches(ei.getName(), tail(ed.getPath()))) - try { - match = sliceMatches(hostContext, ei.getElement(), ei.getPath(), slicer, ed, profile, errors, sliceInfo, stack); - if (match) { - ei.slice = slicer; - - // Since a defined slice was found, this is not an additional (undefined) slice. - ei.additionalSlice = false; - } else if (ei.slice == null) { - // if the specified slice is undefined, keep track of the fact this is an additional (undefined) slice, but only if a slice wasn't found previously - ei.additionalSlice = true; - } - } catch (FHIRException e) { - rule(errors, IssueType.PROCESSING, ei.line(), ei.col(), ei.getPath(), false, e.getMessage()); - unsupportedSlicing = true; - childUnsupportedSlicing = true; - } - } - if (match) { - boolean isOk = ei.definition == null || ei.definition == slicer || (ei.definition.getPath().endsWith("[x]") && ed.getPath().startsWith(ei.definition.getPath().replace("[x]", ""))); - if (rule(errors, IssueType.INVALID, ei.line(), ei.col(), ei.getPath(), isOk, "Profile " + profile.getUrl() + ", Element matches more than one slice - " + (ei.definition == null || !ei.definition.hasSliceName() ? "" : ei.definition.getSliceName()) + ", " + (ed.hasSliceName() ? ed.getSliceName() : ""))) { - ei.definition = ed; - if (ei.slice == null) { - ei.index = i; - } else { - ei.index = sliceOffset; - ei.sliceindex = i - (sliceOffset + 1); - } - } - } else if (childUnsupportedSlicing) { - problematicPaths.add(ed.getPath()); - } - return unsupportedSlicing; - } - - private ElementDefinition getElementByTail(StructureDefinition p, String tail) throws DefinitionException { - if (tail == null) - return p.getSnapshot().getElement().get(0); - for (ElementDefinition t : p.getSnapshot().getElement()) { - if (tail.equals(t.getId())) - return t; - } - throw new DefinitionException("Unable to find element with id '" + tail + "'"); - } - - private IdStatus idStatusForEntry(Element ep, ElementInfo ei) { - if (isBundleEntry(ei.getPath())) { - Element req = ep.getNamedChild("request"); - Element resp = ep.getNamedChild("response"); - Element fullUrl = ep.getNamedChild("fullUrl"); - Element method = null; - Element url = null; - if (req != null) { - method = req.getNamedChild("method"); - url = req.getNamedChild("url"); - } - if (resp != null) { - return IdStatus.OPTIONAL; - } - if (method == null) { - if (fullUrl == null) - return IdStatus.REQUIRED; - else if (fullUrl.primitiveValue().startsWith("urn:uuid:") || fullUrl.primitiveValue().startsWith("urn:oid:")) - return IdStatus.OPTIONAL; - else - return IdStatus.REQUIRED; - } else { - String s = method.primitiveValue(); - if (s.equals("PUT")) { - if (url == null) - return IdStatus.REQUIRED; - else - return IdStatus.OPTIONAL; // or maybe prohibited? not clear - } else if (s.equals("POST")) - return IdStatus.OPTIONAL; // this should be prohibited, but see task 9102 - else // actually, we should never get to here; a bundle entry with method get/delete should not have a resource - return IdStatus.OPTIONAL; - } - } else if (isParametersEntry(ei.getPath()) || isBundleOutcome(ei.getPath())) - return IdStatus.OPTIONAL; - else - return IdStatus.REQUIRED; - } - - private void checkInvariants(ValidatorHostContext hostContext, List errors, String path, StructureDefinition profile, ElementDefinition ed, String typename, String typeProfile, Element resource, Element element, boolean onlyNonInherited) throws FHIRException, FHIRException { - if (noInvariantChecks) - return; - - for (ElementDefinitionConstraintComponent inv : ed.getConstraint()) { - if (inv.hasExpression() && (!onlyNonInherited || !inv.hasSource() || profile.getUrl().equals(inv.getSource()))) { - @SuppressWarnings("unchecked") - Set invList = executionId.equals(element.getUserString(EXECUTION_ID)) ? (Set) element.getUserData(EXECUTED_CONSTRAINT_LIST) : null; - if (invList == null) { - invList = new HashSet<>(); - element.setUserData(EXECUTED_CONSTRAINT_LIST, invList); - element.setUserData(EXECUTION_ID, executionId); - } - if (!invList.contains(inv.getKey())) { - invList.add(inv.getKey()); - checkInvariant(hostContext, errors, path, profile, resource, element, inv); - } else { - //System.out.println("Skip "+inv.getKey()+" on "+path); - } - } - } - } - - public void checkInvariant(ValidatorHostContext hostContext, List errors, String path, StructureDefinition profile, Element resource, Element element, ElementDefinitionConstraintComponent inv) throws FHIRException { - ExpressionNode n = (ExpressionNode) inv.getUserData("validator.expression.cache"); - if (n == null) { - long t = System.nanoTime(); - try { - n = fpe.parse(fixExpr(inv.getExpression())); - } catch (FHIRLexerException e) { - throw new FHIRException("Problem processing expression " + inv.getExpression() + " in profile " + profile.getUrl() + " path " + path + ": " + e.getMessage()); - } - fpeTime = fpeTime + (System.nanoTime() - t); - inv.setUserData("validator.expression.cache", n); - } - - String msg; - boolean ok; - try { - long t = System.nanoTime(); - ok = fpe.evaluateToBoolean(hostContext, resource, hostContext.getRootResource(), element, n); - fpeTime = fpeTime + (System.nanoTime() - t); - msg = fpe.forLog(); - } catch (Exception ex) { - ok = false; - msg = ex.getMessage(); - } - if (!ok) { - if (!Utilities.noString(msg)) - msg = " (" + msg + ")"; - if (inv.hasExtension("http://hl7.org/fhir/StructureDefinition/elementdefinition-bestpractice") && - ToolingExtensions.readBooleanExtension(inv, "http://hl7.org/fhir/StructureDefinition/elementdefinition-bestpractice")) { - if (bpWarnings == BestPracticeWarningLevel.Hint) - hint(errors, IssueType.INVARIANT, element.line(), element.col(), path, ok, inv.getKey() + ": " + inv.getHuman() + msg + " [" + n.toString() + "]"); - else if (bpWarnings == BestPracticeWarningLevel.Warning) - warning(errors, IssueType.INVARIANT, element.line(), element.col(), path, ok, inv.getKey() + ": " + inv.getHuman() + msg + " [" + n.toString() + "]"); - else if (bpWarnings == BestPracticeWarningLevel.Error) - rule(errors, IssueType.INVARIANT, element.line(), element.col(), path, ok, inv.getKey() + ": " + inv.getHuman() + msg + " [" + n.toString() + "]"); - } else if (inv.getSeverity() == ConstraintSeverity.ERROR) { - rule(errors, IssueType.INVARIANT, element.line(), element.col(), path, ok, inv.getKey() + ": " + inv.getHuman() + msg + " [" + n.toString() + "]"); - } else if (inv.getSeverity() == ConstraintSeverity.WARNING) { - warning(errors, IssueType.INVARIANT, element.line(), element.line(), path, ok, inv.getKey() + ": " + inv.getHuman() + msg + " [" + n.toString() + "]"); - } - } - } - - private void validateMessage(List errors, List 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"), - "The first entry in a message must be a MessageHeader")) { - List 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 errors, Element element, NodeStack stack) { - // all observations should have a subject, a performer, and a time - - bpCheck(errors, IssueType.INVALID, element.line(), element.col(), stack.getLiteralPath(), element.getNamedChild("subject") != null, "All observations should have a subject"); - List performers = new ArrayList<>(); - element.getNamedChildren("performer", performers); - bpCheck(errors, IssueType.INVALID, element.line(), element.col(), stack.getLiteralPath(), performers.size() > 0, "All observations should have a performer"); - bpCheck(errors, IssueType.INVALID, element.line(), element.col(), stack.getLiteralPath(), element.getNamedChild("effectiveDateTime") != null || element.getNamedChild("effectivePeriod") != null, - "All observations should have an effectiveDateTime or an effectivePeriod"); - } - - /* - * The actual base entry point for internal use (re-entrant) - */ - private void validateResource(ValidatorHostContext hostContext, List errors, Element resource, Element element, StructureDefinition defn, IdStatus idstatus, NodeStack stack) throws FHIRException { - assert stack != null; - assert resource != null; - boolean ok = true; - String resourceName = element.getType(); // todo: consider namespace...? - if (defn == null) { - long t = System.nanoTime(); - defn = element.getProperty().getStructure(); - if (defn == null) - defn = context.fetchResource(StructureDefinition.class, "http://hl7.org/fhir/StructureDefinition/" + resourceName); - sdTime = sdTime + (System.nanoTime() - t); - ok = rule(errors, IssueType.INVALID, element.line(), element.col(), stack.addToLiteralPath(resourceName), defn != null, "No definition found for resource type '" + resourceName + "'"); - } - - String type = defn.getKind() == StructureDefinitionKind.LOGICAL ? defn.getId() : defn.getType(); - // special case: we have a bundle, and the profile is not for a bundle. We'll try the first entry instead - if (!type.equals(resourceName) && resourceName.equals("Bundle")) { - NodeStack first = getFirstEntry(stack); - if (first != null && first.getElement().getType().equals(type)) { - element = first.element; - stack = first; - resourceName = element.getType(); - idstatus = IdStatus.OPTIONAL; // why? - } - // todo: validate everything in this bundle. - } - ok = rule(errors, IssueType.INVALID, -1, -1, stack.getLiteralPath(), type.equals(resourceName), "Specified profile type was '" + type + "', but found type '" + resourceName + "'"); - - if (ok) { - if (idstatus == IdStatus.REQUIRED && (element.getNamedChild("id") == null)) - rule(errors, IssueType.INVALID, element.line(), element.col(), stack.getLiteralPath(), false, "Resource requires an id, but none is present"); - else if (idstatus == IdStatus.PROHIBITED && (element.getNamedChild("id") != null)) - rule(errors, IssueType.INVALID, element.line(), element.col(), stack.getLiteralPath(), false, "Resource has an id, but none is allowed"); - start(hostContext, errors, element, element, defn, stack); // root is both definition and type - } - } - - private NodeStack getFirstEntry(NodeStack bundle) { - List list = new ArrayList(); - bundle.getElement().getNamedChildren("entry", list); - if (list.isEmpty()) - return null; - Element resource = list.get(0).getNamedChild("resource"); - if (resource == null) - return null; + if (ei.definition.hasRepresentation(PropertyRepresentation.TYPEATTR)) + type = ei.getElement().getType(); else { - NodeStack entry = bundle.push(list.get(0), 0, list.get(0).getProperty().getDefinition(), list.get(0).getProperty().getDefinition()); - return entry.push(resource, -1, resource.getProperty().getDefinition(), context.fetchTypeDefinition(resource.fhirType()).getSnapshot().getElementFirstRep()); - } - } - - private void validateSections(List errors, List entries, Element focus, NodeStack stack, String fullUrl, String id) { - List sections = new ArrayList(); - 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 sectionEntries = new ArrayList(); - 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++; + prefix = prefix.substring(0, prefix.length() - 3); + for (TypeRefComponent t : ei.definition.getType()) + if ((prefix + Utilities.capitalize(t.getWorkingCode())).equals(ei.getName())) { + type = t.getWorkingCode(); + // Excluding reference is a kludge to get around versioning issues + if (t.hasProfile() && !type.equals("Reference")) + profiles.add(t.getProfile().get(0).getValue()); } - validateSections(errors, entries, section, localStack, fullUrl, id); - i++; } - } + if (type == null) { + TypeRefComponent trc = ei.definition.getType().get(0); + if (trc.getWorkingCode().equals("Reference")) + type = "Reference"; + else + rule(errors, IssueType.STRUCTURE, ei.line(), ei.col(), stack.getLiteralPath(), false,messages.getString("The_type_of_element__is_not_known_which_is_illegal_Valid_types_at_this_point_are_"), ei.getName(), describeTypes(ei.definition.getType())); + } + } else if (ei.definition.getContentReference() != null) { + typeDefn = resolveNameReference(profile.getSnapshot(), ei.definition.getContentReference()); + } else if (ei.definition.getType().size() == 1 && ("Element".equals(ei.definition.getType().get(0).getWorkingCode()) || "BackboneElement".equals(ei.definition.getType().get(0).getWorkingCode()))) { + if (ei.definition.getType().get(0).hasProfile()) { + CanonicalType pu = ei.definition.getType().get(0).getProfile().get(0); + if (pu.hasExtension(ToolingExtensions.EXT_PROFILE_ELEMENT)) + profiles.add(pu.getValue() + "#" + pu.getExtensionString(ToolingExtensions.EXT_PROFILE_ELEMENT)); + else + profiles.add(pu.getValue()); + } + } - private boolean valueMatchesCriteria(Element value, ElementDefinition criteria, StructureDefinition profile) throws FHIRException { - if (criteria.hasFixed()) { - List msgs = new ArrayList(); - checkFixedValue(msgs, "{virtual}", value, criteria.getFixed(), profile.getUrl(), "value", null); - return msgs.size() == 0; - } else if (criteria.hasBinding() && criteria.getBinding().getStrength() == BindingStrength.REQUIRED && criteria.getBinding().hasValueSet()) { - throw new FHIRException("Unable to resolve slice matching - slice matching by value set not done"); + if (type != null) { + if (type.startsWith("@")) { + ei.definition = findElement(profile, type.substring(1)); + type = null; + } + } + NodeStack localStack = stack.push(ei.getElement(), ei.count, ei.definition, type == null ? typeDefn : resolveType(type, ei.definition.getType())); + if (debug) { + System.out.println(" " + localStack.getLiteralPath()); + } + String localStackLiterapPath = localStack.getLiteralPath(); + String eiPath = ei.getPath(); + assert (eiPath.equals(localStackLiterapPath)) : "ei.path: " + ei.getPath() + " - localStack.getLiteralPath: " + localStackLiterapPath; + boolean thisIsCodeableConcept = false; + String thisExtension = null; + boolean checkDisplay = true; + + checkInvariants(hostContext, errors, profile, ei.definition, resource, ei.getElement(), localStack, true); + + ei.getElement().markValidation(profile, ei.definition); + boolean elementValidated = false; + if (type != null) { + if (isPrimitiveType(type)) { + checkPrimitive(hostContext, errors, ei.getPath(), type, ei.definition, ei.getElement(), profile, stack); } else { - throw new FHIRException("Unable to resolve slice matching - no fixed value or required value set"); + if (ei.definition.hasFixed()) { + checkFixedValue(errors, ei.getPath(), ei.getElement(), ei.definition.getFixed(), profile.getUrl(), ei.definition.getSliceName(), null); + } + if (ei.definition.hasPattern()) { + checkFixedValue(errors, ei.getPath(), ei.getElement(), ei.definition.getPattern(), profile.getUrl(), ei.definition.getSliceName(), null, true); + } + } + if (type.equals("Identifier")) { + checkIdentifier(errors, ei.getPath(), ei.getElement(), ei.definition); + } else if (type.equals("Coding")) { + checkCoding(errors, ei.getPath(), ei.getElement(), profile, ei.definition, inCodeableConcept, checkDisplayInContext, stack); + } else if (type.equals("CodeableConcept")) { + checkDisplay = checkCodeableConcept(errors, ei.getPath(), ei.getElement(), profile, ei.definition, stack); + thisIsCodeableConcept = true; + } else if (type.equals("Reference")) { + checkReference(hostContext, errors, ei.getPath(), ei.getElement(), profile, ei.definition, actualType, localStack); + // We only check extensions if we're not in a complex extension or if the element we're dealing with is not defined as part of that complex extension + } else if (type.equals("Extension")) { + Element eurl = ei.getElement().getNamedChild("url"); + if (rule(errors, IssueType.INVALID, ei.getPath(), eurl != null,messages.getString("Extensionurl_is_required"))) { + String url = eurl.primitiveValue(); + thisExtension = url; + if (rule(errors, IssueType.INVALID, ei.getPath(), !Utilities.noString(url),messages.getString("Extensionurl_is_required"))) { + if (rule(errors, IssueType.INVALID, ei.getPath(), (extensionUrl != null) || Utilities.isAbsoluteUrl(url),messages.getString("Extensionurl_must_be_an_absolute_URL"))) { + checkExtension(hostContext, errors, ei.getPath(), resource, element, ei.getElement(), ei.definition, profile, localStack, stack, extensionUrl); + } + } + } + } else if (type.equals("Resource")) { + validateContains(hostContext, errors, ei.getPath(), ei.definition, definition, resource, ei.getElement(), localStack, idStatusForEntry(element, ei)); // if + elementValidated = true; + // (str.matches(".*([.,/])work\\1$")) + } else if (Utilities.isAbsoluteUrl(type)) { + StructureDefinition defn = context.fetchTypeDefinition(type); + if (defn != null && hasMapping("http://hl7.org/fhir/terminology-pattern", defn, defn.getSnapshot().getElementFirstRep())) { + List txtype = getMapping("http://hl7.org/fhir/terminology-pattern", defn, defn.getSnapshot().getElementFirstRep()); + if (txtype.contains("CodeableConcept")) { + checkTerminologyCodeableConcept(errors, ei.getPath(), ei.getElement(), profile, ei.definition, stack, defn); + thisIsCodeableConcept = true; + } else if (txtype.contains("Coding")) { + checkTerminologyCoding(errors, ei.getPath(), ei.getElement(), profile, ei.definition, inCodeableConcept, checkDisplayInContext, stack, defn); + } + } + } + } else { + if (rule(errors, IssueType.STRUCTURE, ei.line(), ei.col(), stack.getLiteralPath(), ei.definition != null,messages.getString("Unrecognised_Content_"), ei.getName())) + validateElement(hostContext, errors, profile, ei.definition, null, null, resource, ei.getElement(), type, localStack, false, true, null); + } + StructureDefinition p = null; + String tail = null; + if (profiles.isEmpty()) { + if (type != null) { + p = getProfileForType(type, ei.definition.getType()); + + // If dealing with a primitive type, then we need to check the current child against + // the invariants (constraints) on the current element, because otherwise it only gets + // checked against the primary type's invariants: LLoyd + //if (p.getKind() == StructureDefinitionKind.PRIMITIVETYPE) { + // checkInvariants(hostContext, errors, ei.path, profile, ei.definition, null, null, resource, ei.element); + //} + + rule(errors, IssueType.STRUCTURE, ei.line(), ei.col(), ei.getPath(), p != null,messages.getString("Unknown_type_"), type); + } + } else if (profiles.size() == 1) { + String url = profiles.get(0); + if (url.contains("#")) { + tail = url.substring(url.indexOf("#") + 1); + url = url.substring(0, url.indexOf("#")); + } + p = this.context.fetchResource(StructureDefinition.class, url); + rule(errors, IssueType.STRUCTURE, ei.line(), ei.col(), ei.getPath(), p != null,messages.getString("Unknown_profile_"), profiles.get(0)); + } else { + elementValidated = true; + HashMap> goodProfiles = new HashMap>(); + HashMap> badProfiles = new HashMap>(); + for (String typeProfile : profiles) { + String url = typeProfile; + tail = null; + if (url.contains("#")) { + tail = url.substring(url.indexOf("#") + 1); + url = url.substring(0, url.indexOf("#")); + } + p = this.context.fetchResource(StructureDefinition.class, typeProfile); + if (rule(errors, IssueType.STRUCTURE, ei.line(), ei.col(), ei.getPath(), p != null,messages.getString("Unknown_profile_"), typeProfile)) { + List profileErrors = new ArrayList(); + validateElement(hostContext, profileErrors, p, getElementByTail(p, tail), profile, ei.definition, resource, ei.getElement(), type, localStack, thisIsCodeableConcept, checkDisplay, thisExtension); + if (hasErrors(profileErrors)) + badProfiles.put(typeProfile, profileErrors); + else + goodProfiles.put(typeProfile, profileErrors); + } + } + if (goodProfiles.size() == 1) { + errors.addAll(goodProfiles.values().iterator().next()); + } else if (goodProfiles.size() == 0) { + rule(errors, IssueType.STRUCTURE, ei.line(), ei.col(), ei.getPath(), false,messages.getString("Unable_to_find_matching_profile_among_choices_"), StringUtils.join("; ", profiles)); + for (String m : badProfiles.keySet()) { + p = this.context.fetchResource(StructureDefinition.class, m); + for (ValidationMessage message : badProfiles.get(m)) { + message.setMessage(message.getMessage() + " (validating against " + p.getUrl() + (p.hasVersion() ? "|" + p.getVersion() : "") + " [" + p.getName() + "])"); + errors.add(message); + } + } + } else { + warning(errors, IssueType.STRUCTURE, ei.line(), ei.col(), ei.getPath(), false,messages.getString("Found_multiple_matching_profiles_among_choices_"), StringUtils.join("; ", goodProfiles.keySet())); + for (String m : goodProfiles.keySet()) { + p = this.context.fetchResource(StructureDefinition.class, m); + for (ValidationMessage message : goodProfiles.get(m)) { + message.setMessage(message.getMessage() + " (validating against " + p.getUrl() + (p.hasVersion() ? "|" + p.getVersion() : "") + " [" + p.getName() + "])"); + errors.add(message); + } + } + } + } + if (p != null) { + trackUsage(p, hostContext, element); + + if (!elementValidated) { + if (ei.getElement().getSpecial() == SpecialElement.BUNDLE_ENTRY || ei.getElement().getSpecial() == SpecialElement.BUNDLE_OUTCOME || ei.getElement().getSpecial() == SpecialElement.PARAMETER) + validateElement(hostContext, errors, p, getElementByTail(p, tail), profile, ei.definition, ei.getElement(), ei.getElement(), type, localStack, thisIsCodeableConcept, checkDisplay, thisExtension); + else + validateElement(hostContext, errors, p, getElementByTail(p, tail), profile, ei.definition, resource, ei.getElement(), type, localStack, thisIsCodeableConcept, checkDisplay, thisExtension); + } + int index = profile.getSnapshot().getElement().indexOf(ei.definition); + if (index < profile.getSnapshot().getElement().size() - 1) { + String nextPath = profile.getSnapshot().getElement().get(index + 1).getPath(); + if (!nextPath.equals(ei.definition.getPath()) && nextPath.startsWith(ei.definition.getPath())) + validateElement(hostContext, errors, profile, ei.definition, null, null, resource, ei.getElement(), type, localStack, thisIsCodeableConcept, checkDisplay, thisExtension); + } + } + } + } + + private void trackUsage(StructureDefinition profile, ValidatorHostContext hostContext, Element element) { + if (tracker != null) { + tracker.recordProfileUsage(profile, hostContext.getAppContext(), element); + } + } + + private boolean hasMapping(String url, StructureDefinition defn, ElementDefinition elem) { + String id = null; + for (StructureDefinitionMappingComponent m : defn.getMapping()) { + if (url.equals(m.getUri())) { + id = m.getIdentity(); + break; + } + } + if (id != null) { + for (ElementDefinitionMappingComponent m : elem.getMapping()) { + if (id.equals(m.getIdentity())) { + return true; + } + } + + } + return false; + } + + private List getMapping(String url, StructureDefinition defn, ElementDefinition elem) { + List res = new ArrayList<>(); + String id = null; + for (StructureDefinitionMappingComponent m : defn.getMapping()) { + if (url.equals(m.getUri())) { + id = m.getIdentity(); + break; + } + } + if (id != null) { + for (ElementDefinitionMappingComponent m : elem.getMapping()) { + if (id.equals(m.getIdentity())) { + res.add(m.getMap()); + } + } + } + return res; + } + + public void checkMustSupport(StructureDefinition profile, ElementInfo ei) { + String usesMustSupport = profile.getUserString("usesMustSupport"); + if (usesMustSupport == null) { + usesMustSupport = "N"; + for (ElementDefinition pe : profile.getSnapshot().getElement()) { + if (pe.getMustSupport()) { + usesMustSupport = "Y"; + break; + } + } + profile.setUserData("usesMustSupport", usesMustSupport); + } + if (usesMustSupport.equals("Y")) { + String elementSupported = ei.getElement().getUserString("elementSupported"); + if (elementSupported == null || ei.definition.getMustSupport()) + if (ei.definition.getMustSupport()) { + ei.getElement().setUserData("elementSupported", "Y"); } } + } - private boolean yearIsValid(String v) { - if (v == null) { - return false; + public void checkCardinalities(List errors, StructureDefinition profile, Element element, NodeStack stack, + List childDefinitions, List children, List problematicPaths) throws DefinitionException { + // 3. report any definitions that have a cardinality problem + for (ElementDefinition ed : childDefinitions) { + if (ed.getRepresentation().isEmpty()) { // ignore xml attributes + int count = 0; + List slices = null; + if (ed.hasSlicing()) + slices = ProfileUtilities.getSliceList(profile, ed); + for (ElementInfo ei : children) + if (ei.definition == ed) + count++; + else if (slices != null) { + for (ElementDefinition sed : slices) { + if (ei.definition == sed) { + count++; + break; + } + } + } + String location = "Profile " + profile.getUrl() + ", Element '" + stack.getLiteralPath() + "." + tail(ed.getPath()) + (ed.hasSliceName() ? "[" + ed.getSliceName() + (ed.hasLabel() ? " (" + ed.getLabel() + ")" : "") + "]" : "") + "'"; + if (ed.getMin() > 0) { + if (problematicPaths.contains(ed.getPath())) + hint(errors, IssueType.NOTSUPPORTED, element.line(), element.col(), stack.getLiteralPath(), count >= ed.getMin(),messages.getString("_Unable_to_check_minimum_required__due_to_lack_of_slicing_validation"), location, Integer.toString(ed.getMin())); + else + rule(errors, IssueType.STRUCTURE, element.line(), element.col(), stack.getLiteralPath(), count >= ed.getMin(),messages.getString("_minimum_required___but_only_found_"), location, Integer.toString(ed.getMin()), Integer.toString(count)); } + if (ed.hasMax() && !ed.getMax().equals("*")) { + if (problematicPaths.contains(ed.getPath())) + hint(errors, IssueType.NOTSUPPORTED, element.line(), element.col(), stack.getLiteralPath(), count <= Integer.parseInt(ed.getMax()),messages.getString("_Unable_to_check_max_allowed__due_to_lack_of_slicing_validation"), location, ed.getMax()); + else + rule(errors, IssueType.STRUCTURE, element.line(), element.col(), stack.getLiteralPath(), count <= Integer.parseInt(ed.getMax()),messages.getString("_max_allowed___but_found_"), location, ed.getMax(), Integer.toString(count)); + } + } + } + } + + public List assignChildren(ValidatorHostContext hostContext, List errors, StructureDefinition profile, Element resource, + NodeStack stack, List childDefinitions, List children) throws DefinitionException { + // 2. assign children to a definition + // for each definition, for each child, check whether it belongs in the slice + ElementDefinition slicer = null; + boolean unsupportedSlicing = false; + List problematicPaths = new ArrayList(); + String slicingPath = null; + int sliceOffset = 0; + for (int i = 0; i < childDefinitions.size(); i++) { + ElementDefinition ed = childDefinitions.get(i); + boolean childUnsupportedSlicing = false; + boolean process = true; + if (ed.hasSlicing() && !ed.getSlicing().getOrdered()) + slicingPath = ed.getPath(); + else if (slicingPath != null && ed.getPath().equals(slicingPath)) + ; // nothing + else if (slicingPath != null && !ed.getPath().startsWith(slicingPath)) + slicingPath = null; + // where are we with slicing + if (ed.hasSlicing()) { + if (slicer != null && slicer.getPath().equals(ed.getPath())) { + String errorContext = "profile " + profile.getUrl(); + if (!resource.getChildValue("id").isEmpty()) + errorContext += "; instance " + resource.getChildValue("id"); + throw new DefinitionException("Slice encountered midway through set (path = " + slicer.getPath() + ", id = " + slicer.getId() + "); " + errorContext); + } + slicer = ed; + process = false; + sliceOffset = i; + } else if (slicer != null && !slicer.getPath().equals(ed.getPath())) + slicer = null; + + for (ElementInfo ei : children) { + if (ei.sliceInfo == null) { + ei.sliceInfo = new ArrayList<>(); + } + unsupportedSlicing = matchSlice(hostContext, errors, ei.sliceInfo, profile, stack, slicer, unsupportedSlicing, problematicPaths, sliceOffset, i, ed, childUnsupportedSlicing, ei); + } + } + int last = -1; + int lastSlice = -1; + for (ElementInfo ei : children) { + String sliceInfo = ""; + if (slicer != null) + sliceInfo = " (slice: " + slicer.getPath() + ")"; + if (!unsupportedSlicing) + if (ei.additionalSlice && ei.definition != null) { + if (ei.definition.getSlicing().getRules().equals(ElementDefinition.SlicingRules.OPEN) || + ei.definition.getSlicing().getRules().equals(ElementDefinition.SlicingRules.OPENATEND) && true /* TODO: replace "true" with condition to check that this element is at "end" */) { + slicingHint(errors, IssueType.INFORMATIONAL, ei.line(), ei.col(), ei.getPath(), false, "This element does not match any known slice" + (profile == null ? "" : " defined in the profile " + profile.getUrl()), + "This element does not match any known slice" + (profile == null ? "" : " defined in the profile " + profile.getUrl() + ": " + errorSummaryForSlicingAsHtml(ei.sliceInfo))); + } else if (ei.definition.getSlicing().getRules().equals(ElementDefinition.SlicingRules.CLOSED)) { + rule(errors, IssueType.INVALID, ei.line(), ei.col(), ei.getPath(), false, "This element does not match any known slice " + (profile == null ? "" : " defined in the profile " + profile.getUrl() + " and slicing is CLOSED: " + errorSummaryForSlicing(ei.sliceInfo)),messages.getString("This_element_does_not_match_any_known_slice_"), (profile == null ? "" : " defined in the profile " + profile.getUrl() + " and slicing is CLOSED: " + errorSummaryForSlicingAsHtml(ei.sliceInfo))); + } + } else { + // Don't raise this if we're in an abstract profile, like Resource + if (!profile.getAbstract()) + rule(errors, IssueType.NOTSUPPORTED, ei.line(), ei.col(), ei.getPath(), (ei.definition != null),messages.getString("This_element_is_not_allowed_by_the_profile_"), profile.getUrl()); + } + // TODO: Should get the order of elements correct when parsing elements that are XML attributes vs. elements + boolean isXmlAttr = false; + if (ei.definition != null) { + for (Enumeration r : ei.definition.getRepresentation()) { + if (r.getValue() == PropertyRepresentation.XMLATTR) { + isXmlAttr = true; + break; + } + } + } + + 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,messages.getString("As_specified_by_profile__Element__is_out_of_order"), profile.getUrl(), ei.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,messages.getString("As_specified_by_profile__Element__is_out_of_order_in_ordered_slice"), profile.getUrl(), ei.getName()); + if (ei.definition == null || !isXmlAttr) + last = ei.index; + if (ei.slice != null) + lastSlice = ei.sliceindex; + else + lastSlice = -1; + } + return problematicPaths; + } + + public List listChildren(Element element, NodeStack stack) { + // 1. List the children, and remember their exact path (convenience) + List children = new ArrayList(); + ChildIterator iter = new ChildIterator(this, stack.getLiteralPath(), element); + while (iter.next()) + children.add(new ElementInfo(iter.name(), iter.element(), iter.path(), iter.count())); + return children; + } + + public void checkInvariants(ValidatorHostContext hostContext, List errors, StructureDefinition profile, ElementDefinition definition, Element resource, Element element, NodeStack stack, boolean onlyNonInherited) throws FHIRException { + checkInvariants(hostContext, errors, stack.getLiteralPath(), profile, definition, null, null, resource, element, onlyNonInherited); + } + + public boolean matchSlice(ValidatorHostContext hostContext, List errors, List sliceInfo, StructureDefinition profile, NodeStack stack, + ElementDefinition slicer, boolean unsupportedSlicing, List problematicPaths, int sliceOffset, int i, ElementDefinition ed, + boolean childUnsupportedSlicing, ElementInfo ei) { + boolean match = false; + if (slicer == null || slicer == ed) { + match = nameMatches(ei.getName(), tail(ed.getPath())); + } else { + if (nameMatches(ei.getName(), tail(ed.getPath()))) try { - int i = Integer.parseInt(v.substring(0, Math.min(4, v.length()))); - return i >= 1800 && i <= thisYear() + 80; - } catch (NumberFormatException e) { - return false; + match = sliceMatches(hostContext, ei.getElement(), ei.getPath(), slicer, ed, profile, errors, sliceInfo, stack); + if (match) { + ei.slice = slicer; + + // Since a defined slice was found, this is not an additional (undefined) slice. + ei.additionalSlice = false; + } else if (ei.slice == null) { + // if the specified slice is undefined, keep track of the fact this is an additional (undefined) slice, but only if a slice wasn't found previously + ei.additionalSlice = true; + } + } catch (FHIRException e) { + rule(errors, IssueType.PROCESSING, ei.line(), ei.col(), ei.getPath(), false, e.getMessage()); + unsupportedSlicing = true; + childUnsupportedSlicing = true; } } + if (match) { + boolean isOk = ei.definition == null || ei.definition == slicer || (ei.definition.getPath().endsWith("[x]") && ed.getPath().startsWith(ei.definition.getPath().replace("[x]", ""))); + if (rule(errors, IssueType.INVALID, ei.line(), ei.col(), ei.getPath(), isOk,messages.getString("Profile__Element_matches_more_than_one_slice___"), profile.getUrl(), (ei.definition == null || !ei.definition.hasSliceName() ? "" : ei.definition.getSliceName()), (ed.hasSliceName() ? ed.getSliceName() : ""))) { + ei.definition = ed; + if (ei.slice == null) { + ei.index = i; + } else { + ei.index = sliceOffset; + ei.sliceindex = i - (sliceOffset + 1); + } + } + } else if (childUnsupportedSlicing) { + problematicPaths.add(ed.getPath()); + } + return unsupportedSlicing; + } - private int thisYear() { - return Calendar.getInstance().get(Calendar.YEAR); + private ElementDefinition getElementByTail(StructureDefinition p, String tail) throws DefinitionException { + if (tail == null) + return p.getSnapshot().getElement().get(0); + for (ElementDefinition t : p.getSnapshot().getElement()) { + if (tail.equals(t.getId())) + return t; + } + throw new DefinitionException("Unable to find element with id '" + tail + "'"); + } + + private IdStatus idStatusForEntry(Element ep, ElementInfo ei) { + if (isBundleEntry(ei.getPath())) { + Element req = ep.getNamedChild("request"); + Element resp = ep.getNamedChild("response"); + Element fullUrl = ep.getNamedChild("fullUrl"); + Element method = null; + Element url = null; + if (req != null) { + method = req.getNamedChild("method"); + url = req.getNamedChild("url"); + } + if (resp != null) { + return IdStatus.OPTIONAL; + } + if (method == null) { + if (fullUrl == null) + return IdStatus.REQUIRED; + else if (fullUrl.primitiveValue().startsWith("urn:uuid:") || fullUrl.primitiveValue().startsWith("urn:oid:")) + return IdStatus.OPTIONAL; + else + return IdStatus.REQUIRED; + } else { + String s = method.primitiveValue(); + if (s.equals("PUT")) { + if (url == null) + return IdStatus.REQUIRED; + else + return IdStatus.OPTIONAL; // or maybe prohibited? not clear + } else if (s.equals("POST")) + return IdStatus.OPTIONAL; // this should be prohibited, but see task 9102 + else // actually, we should never get to here; a bundle entry with method get/delete should not have a resource + return IdStatus.OPTIONAL; + } + } else if (isParametersEntry(ei.getPath()) || isBundleOutcome(ei.getPath())) + return IdStatus.OPTIONAL; + else + return IdStatus.REQUIRED; + } + + private void checkInvariants(ValidatorHostContext hostContext, List errors, String path, StructureDefinition profile, ElementDefinition ed, String typename, String typeProfile, Element resource, Element element, boolean onlyNonInherited) throws FHIRException, FHIRException { + if (noInvariantChecks) + return; + + for (ElementDefinitionConstraintComponent inv : ed.getConstraint()) { + if (inv.hasExpression() && (!onlyNonInherited || !inv.hasSource() || profile.getUrl().equals(inv.getSource()))) { + @SuppressWarnings("unchecked") + Set invList = executionId.equals(element.getUserString(EXECUTION_ID)) ? (Set) element.getUserData(EXECUTED_CONSTRAINT_LIST) : null; + if (invList == null) { + invList = new HashSet<>(); + element.setUserData(EXECUTED_CONSTRAINT_LIST, invList); + element.setUserData(EXECUTION_ID, executionId); + } + if (!invList.contains(inv.getKey())) { + invList.add(inv.getKey()); + checkInvariant(hostContext, errors, path, profile, resource, element, inv); + } else { + //System.out.println("Skip "+inv.getKey()+" on "+path); + } + } + } + } + + public void checkInvariant(ValidatorHostContext hostContext, List errors, String path, StructureDefinition profile, Element resource, Element element, ElementDefinitionConstraintComponent inv) throws FHIRException { + ExpressionNode n = (ExpressionNode) inv.getUserData("validator.expression.cache"); + if (n == null) { + long t = System.nanoTime(); + try { + n = fpe.parse(fixExpr(inv.getExpression())); + } catch (FHIRLexerException e) { + throw new FHIRException("Problem processing expression " + inv.getExpression() + " in profile " + profile.getUrl() + " path " + path + ": " + e.getMessage()); + } + fpeTime = fpeTime + (System.nanoTime() - t); + inv.setUserData("validator.expression.cache", n); } - public class NodeStack { - private ElementDefinition definition; - private Element element; - private ElementDefinition extension; - private String literalPath; // xpath format - private List logicalPaths; // dotted format, various entry points - private NodeStack parent; - private ElementDefinition type; - private String workingLang; + String msg; + boolean ok; + try { + long t = System.nanoTime(); + ok = fpe.evaluateToBoolean(hostContext, resource, hostContext.getRootResource(), element, n); + fpeTime = fpeTime + (System.nanoTime() - t); + msg = fpe.forLog(); + } catch (Exception ex) { + ok = false; + msg = ex.getMessage(); + } + if (!ok) { + if (!Utilities.noString(msg)) + msg = " (" + msg + ")"; + if (inv.hasExtension("http://hl7.org/fhir/StructureDefinition/elementdefinition-bestpractice") && + ToolingExtensions.readBooleanExtension(inv, "http://hl7.org/fhir/StructureDefinition/elementdefinition-bestpractice")) { + if (bpWarnings == BestPracticeWarningLevel.Hint) + hint(errors, IssueType.INVARIANT, element.line(), element.col(), path, ok, inv.getKey() + ": " + inv.getHuman() + msg + " [" + n.toString() + "]"); + else if (bpWarnings == BestPracticeWarningLevel.Warning) + warning(errors, IssueType.INVARIANT, element.line(), element.col(), path, ok, inv.getKey() + ": " + inv.getHuman() + msg + " [" + n.toString() + "]"); + else if (bpWarnings == BestPracticeWarningLevel.Error) + rule(errors, IssueType.INVARIANT, element.line(), element.col(), path, ok, inv.getKey() + ": " + inv.getHuman() + msg + " [" + n.toString() + "]"); + } else if (inv.getSeverity() == ConstraintSeverity.ERROR) { + rule(errors, IssueType.INVARIANT, element.line(), element.col(), path, ok, inv.getKey() + ": " + inv.getHuman() + msg + " [" + n.toString() + "]"); + } else if (inv.getSeverity() == ConstraintSeverity.WARNING) { + warning(errors, IssueType.INVARIANT, element.line(), element.line(), path, ok, inv.getKey() + ": " + inv.getHuman() + msg + " [" + n.toString() + "]"); + } + } + } - public NodeStack() { + private void validateMessage(List errors, List 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"),messages.getString("The_first_entry_in_a_message_must_be_a_MessageHeader"))) { + List 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 errors, Element element, NodeStack stack) { + // all observations should have a subject, a performer, and a time + + bpCheck(errors, IssueType.INVALID, element.line(), element.col(), stack.getLiteralPath(), element.getNamedChild("subject") != null, "All observations should have a subject"); + List performers = new ArrayList<>(); + element.getNamedChildren("performer", performers); + bpCheck(errors, IssueType.INVALID, element.line(), element.col(), stack.getLiteralPath(), performers.size() > 0, "All observations should have a performer"); + bpCheck(errors, IssueType.INVALID, element.line(), element.col(), stack.getLiteralPath(), element.getNamedChild("effectiveDateTime") != null || element.getNamedChild("effectivePeriod") != null, + "All observations should have an effectiveDateTime or an effectivePeriod"); + } + + /* + * The actual base entry point for internal use (re-entrant) + */ + private void validateResource(ValidatorHostContext hostContext, List errors, Element resource, Element element, StructureDefinition defn, IdStatus idstatus, NodeStack stack) throws FHIRException { + assert stack != null; + assert resource != null; + boolean ok = true; + String resourceName = element.getType(); // todo: consider namespace...? + if (defn == null) { + long t = System.nanoTime(); + defn = element.getProperty().getStructure(); + if (defn == null) + defn = context.fetchResource(StructureDefinition.class, "http://hl7.org/fhir/StructureDefinition/" + resourceName); + sdTime = sdTime + (System.nanoTime() - t); + ok = rule(errors, IssueType.INVALID, element.line(), element.col(), stack.addToLiteralPath(resourceName), defn != null,messages.getString("No_definition_found_for_resource_type_"), resourceName); + } + + String type = defn.getKind() == StructureDefinitionKind.LOGICAL ? defn.getId() : defn.getType(); + // special case: we have a bundle, and the profile is not for a bundle. We'll try the first entry instead + if (!type.equals(resourceName) && resourceName.equals("Bundle")) { + NodeStack first = getFirstEntry(stack); + if (first != null && first.getElement().getType().equals(type)) { + element = first.element; + stack = first; + resourceName = element.getType(); + idstatus = IdStatus.OPTIONAL; // why? + } + // todo: validate everything in this bundle. + } + ok = rule(errors, IssueType.INVALID, -1, -1, stack.getLiteralPath(), type.equals(resourceName),messages.getString("Specified_profile_type_was__but_found_type_"), type, resourceName); + + if (ok) { + if (idstatus == IdStatus.REQUIRED && (element.getNamedChild("id") == null)) + rule(errors, IssueType.INVALID, element.line(), element.col(), stack.getLiteralPath(), false,messages.getString("Resource_requires_an_id_but_none_is_present")); + else if (idstatus == IdStatus.PROHIBITED && (element.getNamedChild("id") != null)) + rule(errors, IssueType.INVALID, element.line(), element.col(), stack.getLiteralPath(), false,messages.getString("Resource_has_an_id_but_none_is_allowed")); + start(hostContext, errors, element, element, defn, stack); // root is both definition and type + } + } + + private NodeStack getFirstEntry(NodeStack bundle) { + List list = new ArrayList(); + bundle.getElement().getNamedChildren("entry", list); + if (list.isEmpty()) + return null; + Element resource = list.get(0).getNamedChild("resource"); + if (resource == null) + return null; + else { + NodeStack entry = bundle.push(list.get(0), 0, list.get(0).getProperty().getDefinition(), list.get(0).getProperty().getDefinition()); + return entry.push(resource, -1, resource.getProperty().getDefinition(), context.fetchTypeDefinition(resource.fhirType()).getSnapshot().getElementFirstRep()); + } + } + + private void validateSections(List errors, List entries, Element focus, NodeStack stack, String fullUrl, String id) { + List sections = new ArrayList(); + 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 sectionEntries = new ArrayList(); + 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 msgs = new ArrayList(); + checkFixedValue(msgs, "{virtual}", value, criteria.getFixed(), profile.getUrl(), "value", null); + return msgs.size() == 0; + } else if (criteria.hasBinding() && criteria.getBinding().getStrength() == BindingStrength.REQUIRED && criteria.getBinding().hasValueSet()) { + throw new FHIRException("Unable to resolve slice matching - slice matching by value set not done"); + } else { + throw new FHIRException("Unable to resolve slice matching - no fixed value or required value set"); + } + } + + private boolean yearIsValid(String v) { + if (v == null) { + return false; + } + try { + int i = Integer.parseInt(v.substring(0, Math.min(4, v.length()))); + return i >= 1800 && i <= thisYear() + 80; + } catch (NumberFormatException e) { + return false; + } + } + + private int thisYear() { + return Calendar.getInstance().get(Calendar.YEAR); + } + + public class NodeStack { + private ElementDefinition definition; + private Element element; + private ElementDefinition extension; + private String literalPath; // xpath format + private List logicalPaths; // dotted format, various entry points + private NodeStack parent; + private ElementDefinition type; + private String workingLang; + + public NodeStack() { + } + + public NodeStack(Element element) { + this.element = element; + literalPath = element.getName(); + workingLang = validationLanguage; + if (!element.getName().equals(element.fhirType())) { + logicalPaths = new ArrayList<>(); + logicalPaths.add(element.fhirType()); + } + } + + public NodeStack(Element element, String refPath) { + this.element = element; + literalPath = refPath + "->" + element.getName(); + workingLang = validationLanguage; + } + + public String addToLiteralPath(String... path) { + StringBuilder b = new StringBuilder(); + b.append(getLiteralPath()); + for (String p : path) { + if (p.startsWith(":")) { + b.append("["); + b.append(p.substring(1)); + b.append("]"); + } else { + b.append("."); + b.append(p); } + } + return b.toString(); + } - public NodeStack(Element element) { - this.element = element; - literalPath = element.getName(); - workingLang = validationLanguage; - if (!element.getName().equals(element.fhirType())) { - logicalPaths = new ArrayList<>(); - logicalPaths.add(element.fhirType()); + private ElementDefinition getDefinition() { + return definition; + } + + private Element getElement() { + return element; + } + + protected String getLiteralPath() { + return literalPath == null ? "" : literalPath; + } + + private List getLogicalPaths() { + return logicalPaths == null ? new ArrayList() : logicalPaths; + } + + private ElementDefinition getType() { + return type; + } + + private NodeStack pushTarget(Element element, int count, ElementDefinition definition, ElementDefinition type) { + return pushInternal(element, count, definition, type, "->"); + } + + private NodeStack push(Element element, int count, ElementDefinition definition, ElementDefinition type) { + return pushInternal(element, count, definition, type, "."); + } + + private NodeStack pushInternal(Element element, int count, ElementDefinition definition, ElementDefinition type, String sep) { + NodeStack res = new NodeStack(); + res.parent = this; + res.workingLang = this.workingLang; + res.element = element; + res.definition = definition; + res.literalPath = getLiteralPath() + sep + element.getName(); + if (count > -1) + res.literalPath = res.literalPath + "[" + Integer.toString(count) + "]"; + else if (element.getSpecial() == null && element.getProperty().isList()) + res.literalPath = res.literalPath + "[0]"; + else if (element.getProperty().isChoice()) { + String n = res.literalPath.substring(res.literalPath.lastIndexOf(".") + 1); + String en = element.getProperty().getName(); + en = en.substring(0, en.length() - 3); + String t = n.substring(en.length()); + if (isPrimitiveType(Utilities.uncapitalize(t))) + t = Utilities.uncapitalize(t); + res.literalPath = res.literalPath.substring(0, res.literalPath.lastIndexOf(".")) + "." + en + ".ofType(" + t + ")"; + } + res.logicalPaths = new ArrayList(); + if (type != null) { + // type will be bull if we on a stitching point of a contained resource, or if.... + res.type = type; + String tn = res.type.getPath(); + String t = tail(definition.getPath()); + if ("Resource".equals(tn)) { + tn = element.fhirType(); + } + for (String lp : getLogicalPaths()) { + res.logicalPaths.add(lp + "." + t); + if (t.endsWith("[x]")) + res.logicalPaths.add(lp + "." + t.substring(0, t.length() - 3) + type.getPath()); + } + res.logicalPaths.add(tn); + } else if (definition != null) { + for (String lp : getLogicalPaths()) { + res.logicalPaths.add(lp + "." + element.getName()); + } + res.logicalPaths.add(definition.typeSummary()); + } else + res.logicalPaths.addAll(getLogicalPaths()); + return res; + } + + private void setType(ElementDefinition type) { + this.type = type; + } + } + + public String reportTimes() { + String s = String.format("Times (ms): overall = %d, tx = %d, sd = %d, load = %d, fpe = %d", overall / 1000000, txTime / 1000000, sdTime / 1000000, loadTime / 1000000, fpeTime / 1000000); + overall = 0; + txTime = 0; + sdTime = 0; + loadTime = 0; + fpeTime = 0; + return s; + } + + public boolean isNoBindingMsgSuppressed() { + return noBindingMsgSuppressed; + } + + public IResourceValidator setNoBindingMsgSuppressed(boolean noBindingMsgSuppressed) { + this.noBindingMsgSuppressed = noBindingMsgSuppressed; + return this; + } + + + public boolean isNoTerminologyChecks() { + return noTerminologyChecks; + } + + public IResourceValidator setNoTerminologyChecks(boolean noTerminologyChecks) { + this.noTerminologyChecks = noTerminologyChecks; + return this; + } + + public void checkAllInvariants() { + for (StructureDefinition sd : context.allStructures()) { + if (sd.getDerivation() == TypeDerivationRule.SPECIALIZATION) { + for (ElementDefinition ed : sd.getSnapshot().getElement()) { + for (ElementDefinitionConstraintComponent inv : ed.getConstraint()) { + if (inv.hasExpression()) { + try { + ExpressionNode n = (ExpressionNode) inv.getUserData("validator.expression.cache"); + if (n == null) { + n = fpe.parse(fixExpr(inv.getExpression())); + inv.setUserData("validator.expression.cache", n); + } + fpe.check(null, sd.getKind() == StructureDefinitionKind.RESOURCE ? sd.getType() : "DomainResource", ed.getPath(), n); + } catch (Exception e) { + System.out.println("Error processing structure [" + sd.getId() + "] path " + ed.getPath() + ":" + inv.getKey() + " ('" + inv.getExpression() + "'): " + e.getMessage()); + } } + } } + } + } + } - public NodeStack(Element element, String refPath) { - this.element = element; - literalPath = refPath + "->" + element.getName(); - workingLang = validationLanguage; - } - - public String addToLiteralPath(String... path) { - StringBuilder b = new StringBuilder(); - b.append(getLiteralPath()); - for (String p : path) { - if (p.startsWith(":")) { - b.append("["); - b.append(p.substring(1)); - b.append("]"); - } else { - b.append("."); - b.append(p); - } - } - return b.toString(); - } - - private ElementDefinition getDefinition() { - return definition; - } - - private Element getElement() { - return element; - } - - protected String getLiteralPath() { - return literalPath == null ? "" : literalPath; - } - - private List getLogicalPaths() { - return logicalPaths == null ? new ArrayList() : logicalPaths; - } - - private ElementDefinition getType() { - return type; - } - - private NodeStack pushTarget(Element element, int count, ElementDefinition definition, ElementDefinition type) { - return pushInternal(element, count, definition, type, "->"); - } - - private NodeStack push(Element element, int count, ElementDefinition definition, ElementDefinition type) { - return pushInternal(element, count, definition, type, "."); - } - - private NodeStack pushInternal(Element element, int count, ElementDefinition definition, ElementDefinition type, String sep) { - NodeStack res = new NodeStack(); - res.parent = this; - res.workingLang = this.workingLang; - res.element = element; - res.definition = definition; - res.literalPath = getLiteralPath() + sep + element.getName(); - if (count > -1) - res.literalPath = res.literalPath + "[" + Integer.toString(count) + "]"; - else if (element.getSpecial() == null && element.getProperty().isList()) - res.literalPath = res.literalPath + "[0]"; - else if (element.getProperty().isChoice()) { - String n = res.literalPath.substring(res.literalPath.lastIndexOf(".") + 1); - String en = element.getProperty().getName(); - en = en.substring(0, en.length() - 3); - String t = n.substring(en.length()); - if (isPrimitiveType(Utilities.uncapitalize(t))) - t = Utilities.uncapitalize(t); - res.literalPath = res.literalPath.substring(0, res.literalPath.lastIndexOf(".")) + "." + en + ".ofType(" + t + ")"; - } - res.logicalPaths = new ArrayList(); - if (type != null) { - // type will be bull if we on a stitching point of a contained resource, or if.... - res.type = type; - String tn = res.type.getPath(); - String t = tail(definition.getPath()); - if ("Resource".equals(tn)) { - tn = element.fhirType(); - } - for (String lp : getLogicalPaths()) { - res.logicalPaths.add(lp + "." + t); - if (t.endsWith("[x]")) - res.logicalPaths.add(lp + "." + t.substring(0, t.length() - 3) + type.getPath()); - } - res.logicalPaths.add(tn); - } else if (definition != null) { - for (String lp : getLogicalPaths()) { - res.logicalPaths.add(lp + "." + element.getName()); - } - res.logicalPaths.add(definition.typeSummary()); - } else - res.logicalPaths.addAll(getLogicalPaths()); - return res; - } - - private void setType(ElementDefinition type) { - this.type = type; - } + private String fixExpr(String expr) { + // this is a hack work around for past publication of wrong FHIRPath expressions + // R4 + // waiting for 4.0.2 + if ("probability is decimal implies (probability as decimal) <= 100".equals(expr)) { + return "probablility.empty() or ((probability is decimal) implies ((probability as decimal) <= 100))"; } - public String reportTimes() { - String s = String.format("Times (ms): overall = %d, tx = %d, sd = %d, load = %d, fpe = %d", overall / 1000000, txTime / 1000000, sdTime / 1000000, loadTime / 1000000, fpeTime / 1000000); - overall = 0; - txTime = 0; - sdTime = 0; - loadTime = 0; - fpeTime = 0; - return s; - } + // handled in 4.0.1 + if ("(component.empty() and hasMember.empty()) implies (dataAbsentReason or value)".equals(expr)) + return "(component.empty() and hasMember.empty()) implies (dataAbsentReason.exists() or value.exists())"; + if ("isModifier implies isModifierReason.exists()".equals(expr)) + return "(isModifier.exists() and isModifier) implies isModifierReason.exists()"; + if ("(%resource.kind = 'logical' or element.first().path.startsWith(%resource.type)) and (element.tail().not() or element.tail().all(path.startsWith(%resource.differential.element.first().path.replaceMatches('\\\\..*','')&'.')))".equals(expr)) + return "(%resource.kind = 'logical' or element.first().path.startsWith(%resource.type)) and (element.tail().empty() or element.tail().all(path.startsWith(%resource.differential.element.first().path.replaceMatches('\\\\..*','')&'.')))"; + if ("differential.element.all(id) and differential.element.id.trace('ids').isDistinct()".equals(expr)) + return "differential.element.all(id.exists()) and differential.element.id.trace('ids').isDistinct()"; + if ("snapshot.element.all(id) and snapshot.element.id.trace('ids').isDistinct()".equals(expr)) + return "snapshot.element.all(id.exists()) and snapshot.element.id.trace('ids').isDistinct()"; - public boolean isNoBindingMsgSuppressed() { - return noBindingMsgSuppressed; - } + // R3 + if ("(code or value.empty()) and (system.empty() or system = 'urn:iso:std:iso:4217')".equals(expr)) + return "(code.exists() or value.empty()) and (system.empty() or system = 'urn:iso:std:iso:4217')"; + if ("value.empty() or code!=component.code".equals(expr)) + return "value.empty() or (code in component.code).not()"; + if ("(code or value.empty()) and (system.empty() or system = %ucum) and (value.empty() or value > 0)".equals(expr)) + return "(code.exists() or value.empty()) and (system.empty() or system = %ucum) and (value.empty() or value > 0)"; + if ("element.all(definition and min and max)".equals(expr)) + return "element.all(definition.exists() and min.exists() and max.exists())"; + if ("telecom or endpoint".equals(expr)) + return "telecom.exists() or endpoint.exists()"; + if ("(code or value.empty()) and (system.empty() or system = %ucum) and (value.empty() or value > 0)".equals(expr)) + return "(code.exists() or value.empty()) and (system.empty() or system = %ucum) and (value.empty() or value > 0)"; + if ("searchType implies type = 'string'".equals(expr)) + return "searchType.exists() implies type = 'string'"; + if ("abatement.empty() or (abatement as boolean).not() or clinicalStatus='resolved' or clinicalStatus='remission' or clinicalStatus='inactive'".equals(expr)) + return "abatement.empty() or (abatement is boolean).not() or (abatement as boolean).not() or (clinicalStatus = 'resolved') or (clinicalStatus = 'remission') or (clinicalStatus = 'inactive')"; + if ("(component.empty() and related.empty()) implies (dataAbsentReason or value)".equals(expr)) + return "(component.empty() and related.empty()) implies (dataAbsentReason.exists() or value.exists())"; - public IResourceValidator setNoBindingMsgSuppressed(boolean noBindingMsgSuppressed) { - this.noBindingMsgSuppressed = noBindingMsgSuppressed; - return this; - } + if ("".equals(expr)) + return ""; + return expr; + } + public IEvaluationContext getExternalHostServices() { + return externalHostServices; + } - public boolean isNoTerminologyChecks() { - return noTerminologyChecks; - } + public String getValidationLanguage() { + return validationLanguage; + } - public IResourceValidator setNoTerminologyChecks(boolean noTerminologyChecks) { - this.noTerminologyChecks = noTerminologyChecks; - return this; - } + public void setValidationLanguage(String validationLanguage) { + this.validationLanguage = validationLanguage; + } - public void checkAllInvariants() { - for (StructureDefinition sd : context.allStructures()) { - if (sd.getDerivation() == TypeDerivationRule.SPECIALIZATION) { - for (ElementDefinition ed : sd.getSnapshot().getElement()) { - for (ElementDefinitionConstraintComponent inv : ed.getConstraint()) { - if (inv.hasExpression()) { - try { - ExpressionNode n = (ExpressionNode) inv.getUserData("validator.expression.cache"); - if (n == null) { - n = fpe.parse(fixExpr(inv.getExpression())); - inv.setUserData("validator.expression.cache", n); - } - fpe.check(null, sd.getKind() == StructureDefinitionKind.RESOURCE ? sd.getType() : "DomainResource", ed.getPath(), n); - } catch (Exception e) { - System.out.println("Error processing structure [" + sd.getId() + "] path " + ed.getPath() + ":" + inv.getKey() + " ('" + inv.getExpression() + "'): " + e.getMessage()); - } - } - } - } - } - } - } + public boolean isDebug() { + return debug; + } - private String fixExpr(String expr) { - // this is a hack work around for past publication of wrong FHIRPath expressions - // R4 - // waiting for 4.0.2 - if ("probability is decimal implies (probability as decimal) <= 100".equals(expr)) { - return "probablility.empty() or ((probability is decimal) implies ((probability as decimal) <= 100))"; - } - - // handled in 4.0.1 - if ("(component.empty() and hasMember.empty()) implies (dataAbsentReason or value)".equals(expr)) - return "(component.empty() and hasMember.empty()) implies (dataAbsentReason.exists() or value.exists())"; - if ("isModifier implies isModifierReason.exists()".equals(expr)) - return "(isModifier.exists() and isModifier) implies isModifierReason.exists()"; - if ("(%resource.kind = 'logical' or element.first().path.startsWith(%resource.type)) and (element.tail().not() or element.tail().all(path.startsWith(%resource.differential.element.first().path.replaceMatches('\\\\..*','')&'.')))".equals(expr)) - return "(%resource.kind = 'logical' or element.first().path.startsWith(%resource.type)) and (element.tail().empty() or element.tail().all(path.startsWith(%resource.differential.element.first().path.replaceMatches('\\\\..*','')&'.')))"; - if ("differential.element.all(id) and differential.element.id.trace('ids').isDistinct()".equals(expr)) - return "differential.element.all(id.exists()) and differential.element.id.trace('ids').isDistinct()"; - if ("snapshot.element.all(id) and snapshot.element.id.trace('ids').isDistinct()".equals(expr)) - return "snapshot.element.all(id.exists()) and snapshot.element.id.trace('ids').isDistinct()"; - - // R3 - if ("(code or value.empty()) and (system.empty() or system = 'urn:iso:std:iso:4217')".equals(expr)) - return "(code.exists() or value.empty()) and (system.empty() or system = 'urn:iso:std:iso:4217')"; - if ("value.empty() or code!=component.code".equals(expr)) - return "value.empty() or (code in component.code).not()"; - if ("(code or value.empty()) and (system.empty() or system = %ucum) and (value.empty() or value > 0)".equals(expr)) - return "(code.exists() or value.empty()) and (system.empty() or system = %ucum) and (value.empty() or value > 0)"; - if ("element.all(definition and min and max)".equals(expr)) - return "element.all(definition.exists() and min.exists() and max.exists())"; - if ("telecom or endpoint".equals(expr)) - return "telecom.exists() or endpoint.exists()"; - if ("(code or value.empty()) and (system.empty() or system = %ucum) and (value.empty() or value > 0)".equals(expr)) - return "(code.exists() or value.empty()) and (system.empty() or system = %ucum) and (value.empty() or value > 0)"; - if ("searchType implies type = 'string'".equals(expr)) - return "searchType.exists() implies type = 'string'"; - if ("abatement.empty() or (abatement as boolean).not() or clinicalStatus='resolved' or clinicalStatus='remission' or clinicalStatus='inactive'".equals(expr)) - return "abatement.empty() or (abatement is boolean).not() or (abatement as boolean).not() or (clinicalStatus = 'resolved') or (clinicalStatus = 'remission') or (clinicalStatus = 'inactive')"; - if ("(component.empty() and related.empty()) implies (dataAbsentReason or value)".equals(expr)) - return "(component.empty() and related.empty()) implies (dataAbsentReason.exists() or value.exists())"; - - if ("".equals(expr)) - return ""; - return expr; - } - - public IEvaluationContext getExternalHostServices() { - return externalHostServices; - } - - public String getValidationLanguage() { - return validationLanguage; - } - - public void setValidationLanguage(String validationLanguage) { - this.validationLanguage = validationLanguage; - } - - public boolean isDebug() { - return debug; - } - - public void setDebug(boolean debug) { - this.debug = debug; - } + public void setDebug(boolean debug) { + this.debug = debug; + } }