package org.hl7.fhir.dstu3.validation; import static org.apache.commons.lang3.StringUtils.isBlank; import static org.apache.commons.lang3.StringUtils.isNotBlank; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.commons.lang3.StringUtils; import org.hl7.fhir.dstu3.conformance.ProfileUtilities; import org.hl7.fhir.dstu3.context.IWorkerContext; import org.hl7.fhir.dstu3.context.IWorkerContext.ValidationResult; import org.hl7.fhir.dstu3.elementmodel.Element; import org.hl7.fhir.dstu3.elementmodel.Element.SpecialElement; import org.hl7.fhir.dstu3.elementmodel.JsonParser; import org.hl7.fhir.dstu3.elementmodel.Manager; import org.hl7.fhir.dstu3.elementmodel.Manager.FhirFormat; import org.hl7.fhir.dstu3.elementmodel.ObjectConverter; import org.hl7.fhir.dstu3.elementmodel.ParserBase; import org.hl7.fhir.dstu3.elementmodel.ParserBase.ValidationPolicy; import org.hl7.fhir.dstu3.elementmodel.XmlParser; import org.hl7.fhir.dstu3.formats.FormatUtilities; import org.hl7.fhir.dstu3.model.Address; import org.hl7.fhir.dstu3.model.Attachment; import org.hl7.fhir.dstu3.model.BooleanType; import org.hl7.fhir.dstu3.model.Bundle; import org.hl7.fhir.dstu3.model.Bundle.BundleEntryComponent; import org.hl7.fhir.dstu3.model.CodeSystem; import org.hl7.fhir.dstu3.model.CodeSystem.ConceptDefinitionComponent; import org.hl7.fhir.dstu3.model.CodeableConcept; import org.hl7.fhir.dstu3.model.Coding; import org.hl7.fhir.dstu3.model.ContactPoint; import org.hl7.fhir.dstu3.model.DateType; import org.hl7.fhir.dstu3.model.DecimalType; import org.hl7.fhir.dstu3.model.DomainResource; import org.hl7.fhir.dstu3.model.ElementDefinition; import org.hl7.fhir.dstu3.model.ElementDefinition.AggregationMode; import org.hl7.fhir.dstu3.model.ElementDefinition.ConstraintSeverity; import org.hl7.fhir.dstu3.model.ElementDefinition.DiscriminatorType; import org.hl7.fhir.dstu3.model.ElementDefinition.ElementDefinitionBindingComponent; import org.hl7.fhir.dstu3.model.ElementDefinition.ElementDefinitionConstraintComponent; import org.hl7.fhir.dstu3.model.ElementDefinition.ElementDefinitionSlicingDiscriminatorComponent; import org.hl7.fhir.dstu3.model.ElementDefinition.PropertyRepresentation; import org.hl7.fhir.dstu3.model.ElementDefinition.TypeRefComponent; import org.hl7.fhir.dstu3.model.Enumeration; import org.hl7.fhir.dstu3.model.Enumerations.BindingStrength; import org.hl7.fhir.dstu3.model.ExpressionNode; import org.hl7.fhir.dstu3.model.Extension; import org.hl7.fhir.dstu3.model.HumanName; import org.hl7.fhir.dstu3.model.Identifier; import org.hl7.fhir.dstu3.model.IntegerType; import org.hl7.fhir.dstu3.model.Period; import org.hl7.fhir.dstu3.model.Quantity; import org.hl7.fhir.dstu3.model.Questionnaire; import org.hl7.fhir.dstu3.model.Questionnaire.QuestionnaireItemComponent; import org.hl7.fhir.dstu3.model.Questionnaire.QuestionnaireItemOptionComponent; import org.hl7.fhir.dstu3.model.Questionnaire.QuestionnaireItemType; import org.hl7.fhir.dstu3.model.Range; import org.hl7.fhir.dstu3.model.Ratio; import org.hl7.fhir.dstu3.model.Reference; import org.hl7.fhir.dstu3.model.Resource; import org.hl7.fhir.dstu3.model.SampledData; import org.hl7.fhir.dstu3.model.StringType; import org.hl7.fhir.dstu3.model.StructureDefinition; import org.hl7.fhir.dstu3.model.StructureDefinition.ExtensionContext; import org.hl7.fhir.dstu3.model.StructureDefinition.StructureDefinitionKind; import org.hl7.fhir.dstu3.model.StructureDefinition.StructureDefinitionSnapshotComponent; import org.hl7.fhir.dstu3.model.StructureDefinition.TypeDerivationRule; import org.hl7.fhir.dstu3.model.TimeType; import org.hl7.fhir.dstu3.model.Timing; import org.hl7.fhir.dstu3.model.Type; import org.hl7.fhir.dstu3.model.UriType; import org.hl7.fhir.dstu3.model.ValueSet; import org.hl7.fhir.dstu3.model.ValueSet.ValueSetExpansionContainsComponent; import org.hl7.fhir.dstu3.utils.FHIRLexer.FHIRLexerException; import org.hl7.fhir.dstu3.utils.FHIRPathEngine; import org.hl7.fhir.dstu3.utils.FHIRPathEngine.IEvaluationContext; import org.hl7.fhir.dstu3.utils.IResourceValidator; import org.hl7.fhir.dstu3.utils.ToolingExtensions; import org.hl7.fhir.dstu3.utils.ValidationProfileSet; import org.hl7.fhir.dstu3.utils.ValidationProfileSet.ProfileRegistration; import org.hl7.fhir.exceptions.DefinitionException; import org.hl7.fhir.exceptions.FHIRException; import org.hl7.fhir.exceptions.FHIRFormatError; import org.hl7.fhir.exceptions.PathEngineException; import org.hl7.fhir.exceptions.TerminologyServiceException; import org.hl7.fhir.utilities.CommaSeparatedStringBuilder; import org.hl7.fhir.utilities.Utilities; import org.hl7.fhir.utilities.validation.ValidationMessage; import org.hl7.fhir.utilities.validation.ValidationMessage.IssueSeverity; import org.hl7.fhir.utilities.validation.ValidationMessage.IssueType; import org.hl7.fhir.utilities.validation.ValidationMessage.Source; import org.hl7.fhir.utilities.xhtml.NodeType; import org.hl7.fhir.utilities.xhtml.XhtmlNode; import org.w3c.dom.Document; import org.w3c.dom.Node; import com.google.gson.Gson; import com.google.gson.JsonObject; import ca.uhn.fhir.util.ObjectUtil; /** * Thinking of using this in a java program? Don't! * You should use on of the wrappers instead. Either in HAPI, or use ValidationEngine * * @author Grahame Grieve * */ /* * todo: * check urn's don't start oid: or uuid: */ public class InstanceValidator extends BaseValidator implements IResourceValidator { private IWorkerContext context; private FHIRPathEngine fpe; // configuration items private CheckDisplayOption checkDisplay; private boolean anyExtensionsAllowed; private boolean errorForUnknownProfiles; private boolean noInvariantChecks; private boolean noTerminologyChecks; private BestPracticeWarningLevel bpWarnings; private List<String> extensionDomains = new ArrayList<String>(); private IdStatus resourceIdRule; private boolean allowXsiLocation; // used during the build process to keep the overall volume of messages down private boolean suppressLoincSnomedMessages; private Bundle logical; // 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 HashMap<Element, ResourceProfiles> resourceProfilesMap; private IValidatorResourceFetcher fetcher; long time = 0; /* * Keeps track of whether a particular profile has been checked or not yet */ private class ProfileUsage { private StructureDefinition profile; private boolean checked; public ProfileUsage(StructureDefinition profile) { this.profile = profile; this.checked = false; } public boolean isChecked() { return checked; } public void setChecked() { this.checked = true; } public StructureDefinition getProfile() { return profile; } } /* * Keeps track of all profiles associated with a resource element and whether the resource has been checked against those profiles yet */ public class ResourceProfiles { private Element resource; private Element owner; private NodeStack stack; private HashMap<StructureDefinition, ProfileUsage> profiles; private boolean processed; public ResourceProfiles(Element resource, NodeStack stack) { this.resource = resource; if (this.resource.getName().equals("contained")) this.owner = stack.parent.element; else this.owner = resource; this.stack = stack; this.profiles = new HashMap<StructureDefinition, ProfileUsage>(); this.processed = false; } public boolean isProcessed() { return processed; } public void setProcessed() { processed = true; } public NodeStack getStack() { return stack; } public Element getOwner() { return owner; } public boolean hasProfiles() { return !profiles.isEmpty(); } public void addProfiles(List<ValidationMessage> errors, ValidationProfileSet profiles, String path, Element element) throws FHIRException { for (ProfileRegistration profile : profiles.getCanonical()) addProfile(errors, profile.getProfile(), profile.isError(), path, element); } public boolean addProfile(List<ValidationMessage> errors, String profile, boolean error, String path, Element element) { String effectiveProfile = profile; String version = null; if (profile.contains("|")) { effectiveProfile = profile.substring(0, profile.indexOf('|')); version = profile.substring(profile.indexOf('|')+1); } StructureDefinition sd = context.fetchResource(StructureDefinition.class, effectiveProfile); if (warningOrError(error, errors, IssueType.INVALID, element.line(), element.col(), path, sd != null, "StructureDefinition reference \"{0}\" could not be resolved", profile)) { if (rule(errors, IssueType.STRUCTURE, element.line(), element.col(), path, version==null || (sd.getVersion()!=null && sd.getVersion().equals(version)), "Referenced version " + version + " does not match found version " + sd.getVersion() + " for profile " + sd.getUrl(), profile)) { if (rule(errors, IssueType.STRUCTURE, element.line(), element.col(), path, sd.hasSnapshot(), "StructureDefinition has no snapshot - validation is against the snapshot, so it must be provided")) { if (!profiles.containsKey(sd)) { profiles.put(sd, new ProfileUsage(sd)); addAncestorProfiles(sd); return true; } } } } return false; } public void addAncestorProfiles(StructureDefinition sd) { if (sd.getDerivation().equals(StructureDefinition.TypeDerivationRule.CONSTRAINT)) { StructureDefinition parentSd = context.fetchResource(StructureDefinition.class, sd.getBaseDefinition()); if (parentSd != null && !profiles.containsKey(parentSd)) { ProfileUsage pu = new ProfileUsage(parentSd); pu.setChecked(); // We're going to check the child, so no need to check the parent profiles.put(parentSd, pu); } } } public List<ProfileUsage> uncheckedProfiles() { List<ProfileUsage> uncheckedProfiles = new ArrayList<ProfileUsage>(); for (ProfileUsage profileUsage : profiles.values()) { if (!profileUsage.isChecked()) uncheckedProfiles.add(profileUsage); } return uncheckedProfiles; } public boolean hasUncheckedProfiles() { return !uncheckedProfiles().isEmpty(); } public void checkProfile(StructureDefinition profile) { ProfileUsage profileUsage = profiles.get(profile); if (profileUsage==null) throw new Error("Can't check profile that hasn't been added: " + profile.getUrl()); else profileUsage.setChecked(); } } public InstanceValidator(IWorkerContext theContext, IEvaluationContext hostServices) { super(); this.context = theContext; fpe = new FHIRPathEngine(context); fpe.setHostServices(hostServices); source = Source.InstanceValidator; } public InstanceValidator(ValidationEngine engine) { super(); this.context = engine.getContext(); fpe = engine.getFpe(); source = Source.InstanceValidator; } @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; } private boolean allowUnknownExtension(String url) { if (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 explicitly because it wasn't defined in the version of the spec it needs to be used with if (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")) return true; for (String s : extensionDomains) if (url.startsWith(s)) return true; return false; } private void bpCheck(List<ValidationMessage> 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); case Warning: warning(errors, invalid, line, col, literalPath, test, message); case Hint: hint(errors, invalid, line, col, literalPath, test, message); default: // do nothing } } } @Override public org.hl7.fhir.dstu3.elementmodel.Element validate(Object appContext, List<ValidationMessage> errors, InputStream stream, FhirFormat format) throws FHIRException, IOException { return validate(appContext, errors, stream, format, new ValidationProfileSet()); } @Override public org.hl7.fhir.dstu3.elementmodel.Element validate(Object appContext, List<ValidationMessage> errors, InputStream stream, FhirFormat format, String profile) throws FHIRException, IOException { return validate(appContext, errors, stream, format, new ValidationProfileSet(profile, true)); } @Override public org.hl7.fhir.dstu3.elementmodel.Element validate(Object appContext, List<ValidationMessage> errors, InputStream stream, FhirFormat format, StructureDefinition profile) throws FHIRException, IOException { return validate(appContext, errors, stream, format, new ValidationProfileSet(profile)); } @Override public org.hl7.fhir.dstu3.elementmodel.Element validate(Object appContext, List<ValidationMessage> errors, InputStream stream, FhirFormat format, ValidationProfileSet profiles) throws FHIRException, IOException { 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 = parser.parse(stream); loadTime = System.nanoTime() - t; if (e != null) validate(appContext, errors, e, profiles); return e; } @Override public org.hl7.fhir.dstu3.elementmodel.Element validate(Object appContext, List<ValidationMessage> errors, Resource resource) throws FHIRException, IOException { return validate(appContext, errors, resource, new ValidationProfileSet()); } @Override public org.hl7.fhir.dstu3.elementmodel.Element validate(Object appContext, List<ValidationMessage> errors, Resource resource, String profile) throws FHIRException, IOException { return validate(appContext, errors, resource, new ValidationProfileSet(profile, true)); } @Override public org.hl7.fhir.dstu3.elementmodel.Element validate(Object appContext, List<ValidationMessage> errors, Resource resource, StructureDefinition profile) throws FHIRException, IOException { return validate(appContext, errors, resource, new ValidationProfileSet(profile)); } @Override public org.hl7.fhir.dstu3.elementmodel.Element validate(Object appContext, List<ValidationMessage> errors, Resource resource, ValidationProfileSet profiles) throws FHIRException, IOException { long t = System.nanoTime(); Element e = new ObjectConverter(context).convert(resource); loadTime = System.nanoTime() - t; validate(appContext, errors, e, profiles); return e; } @Override public org.hl7.fhir.dstu3.elementmodel.Element validate(Object appContext, List<ValidationMessage> errors, org.w3c.dom.Element element) throws FHIRException, IOException { return validate(appContext, errors, element, new ValidationProfileSet()); } @Override public org.hl7.fhir.dstu3.elementmodel.Element validate(Object appContext, List<ValidationMessage> errors, org.w3c.dom.Element element, String profile) throws FHIRException, IOException { return validate(appContext, errors, element, new ValidationProfileSet(profile, true)); } @Override public org.hl7.fhir.dstu3.elementmodel.Element validate(Object appContext, List<ValidationMessage> errors, org.w3c.dom.Element element, StructureDefinition profile) throws FHIRException, IOException { return validate(appContext, errors, element, new ValidationProfileSet(profile)); } @Override public org.hl7.fhir.dstu3.elementmodel.Element validate(Object appContext, List<ValidationMessage> errors, org.w3c.dom.Element element, ValidationProfileSet profiles) throws FHIRException, IOException { XmlParser parser = new XmlParser(context); parser.setupValidation(ValidationPolicy.EVERYTHING, errors); long t = System.nanoTime(); Element e = parser.parse(element); loadTime = System.nanoTime() - t; if (e != null) validate(appContext, errors, e, profiles); return e; } @Override public org.hl7.fhir.dstu3.elementmodel.Element validate(Object appContext, List<ValidationMessage> errors, Document document) throws FHIRException, IOException { return validate(appContext, errors, document, new ValidationProfileSet()); } @Override public org.hl7.fhir.dstu3.elementmodel.Element validate(Object appContext, List<ValidationMessage> errors, Document document, String profile) throws FHIRException, IOException { return validate(appContext, errors, document, new ValidationProfileSet(profile, true)); } @Override public org.hl7.fhir.dstu3.elementmodel.Element validate(Object appContext, List<ValidationMessage> errors, Document document, StructureDefinition profile) throws FHIRException, IOException { return validate(appContext, errors, document, new ValidationProfileSet(profile)); } @Override public org.hl7.fhir.dstu3.elementmodel.Element validate(Object appContext, List<ValidationMessage> errors, Document document, ValidationProfileSet profiles) throws FHIRException, IOException { XmlParser parser = new XmlParser(context); parser.setupValidation(ValidationPolicy.EVERYTHING, errors); long t = System.nanoTime(); Element e = parser.parse(document); loadTime = System.nanoTime() - t; if (e != null) validate(appContext, errors, e, profiles); return e; } @Override public org.hl7.fhir.dstu3.elementmodel.Element validate(Object appContext, List<ValidationMessage> errors, JsonObject object) throws FHIRException, IOException { return validate(appContext, errors, object, new ValidationProfileSet()); } @Override public org.hl7.fhir.dstu3.elementmodel.Element validate(Object appContext, List<ValidationMessage> errors, JsonObject object, String profile) throws FHIRException, IOException { return validate(appContext, errors, object, new ValidationProfileSet(profile, true)); } @Override public org.hl7.fhir.dstu3.elementmodel.Element validate(Object appContext, List<ValidationMessage> errors, JsonObject object, StructureDefinition profile) throws FHIRException, IOException { return validate(appContext, errors, object, new ValidationProfileSet(profile)); } @Override public org.hl7.fhir.dstu3.elementmodel.Element validate(Object appContext, List<ValidationMessage> errors, JsonObject object, ValidationProfileSet profiles) throws FHIRException, IOException { 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<ValidationMessage> errors, Element element) throws FHIRException, IOException { ValidationProfileSet profileSet = new ValidationProfileSet(); validate(appContext, errors, element, profileSet); } private void validateRemainder(Object appContext, List<ValidationMessage> errors) throws IOException, FHIRException { boolean processedResource; do { processedResource = false; Set<Element> keys = new HashSet<Element>(); keys.addAll(resourceProfilesMap.keySet()); for (Element resource : keys) { ResourceProfiles rp = resourceProfilesMap.get(resource); if (rp.hasUncheckedProfiles()) { processedResource = true; start(appContext, errors, rp.getOwner(), resource, null, rp.getStack()); } } } while (processedResource); } @Override public void validate(Object appContext, List<ValidationMessage> errors, Element element, String profile) throws FHIRException, IOException { validate(appContext, errors, element, new ValidationProfileSet(profile, true)); } @Override public void validate(Object appContext, List<ValidationMessage> errors, Element element, StructureDefinition profile) throws FHIRException, IOException { validate(appContext, errors, element, new ValidationProfileSet(profile)); } @Override public void validate(Object appContext, List<ValidationMessage> errors, Element element, ValidationProfileSet profiles) throws FHIRException, IOException { // this is the main entry point; all the other entry points end up here coming here... long t = System.nanoTime(); boolean isRoot = false; if (resourceProfilesMap == null) { resourceProfilesMap = new HashMap<Element, ResourceProfiles>(); isRoot = true; } validateResource(appContext, errors, element, element, null, profiles, resourceIdRule, new NodeStack(element)); if (isRoot) { validateRemainder(appContext, errors); resourceProfilesMap = null; } overall = System.nanoTime() - t; } private boolean check(String v1, String v2) { return v1 == null ? Utilities.noString(v1) : v1.equals(v2); } private void checkAddress(List<ValidationMessage> errors, String path, Element focus, Address fixed) { checkFixedValue(errors, path + ".use", focus.getNamedChild("use"), fixed.getUseElement(), "use", focus); checkFixedValue(errors, path + ".text", focus.getNamedChild("text"), fixed.getTextElement(), "text", focus); checkFixedValue(errors, path + ".city", focus.getNamedChild("city"), fixed.getCityElement(), "city", focus); checkFixedValue(errors, path + ".state", focus.getNamedChild("state"), fixed.getStateElement(), "state", focus); checkFixedValue(errors, path + ".country", focus.getNamedChild("country"), fixed.getCountryElement(), "country", focus); checkFixedValue(errors, path + ".zip", focus.getNamedChild("zip"), fixed.getPostalCodeElement(), "postalCode", focus); List<Element> lines = new ArrayList<Element>(); 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), "coding", focus); } } private void checkAttachment(List<ValidationMessage> errors, String path, Element focus, Attachment fixed) { checkFixedValue(errors, path + ".contentType", focus.getNamedChild("contentType"), fixed.getContentTypeElement(), "contentType", focus); checkFixedValue(errors, path + ".language", focus.getNamedChild("language"), fixed.getLanguageElement(), "language", focus); checkFixedValue(errors, path + ".data", focus.getNamedChild("data"), fixed.getDataElement(), "data", focus); checkFixedValue(errors, path + ".url", focus.getNamedChild("url"), fixed.getUrlElement(), "url", focus); checkFixedValue(errors, path + ".size", focus.getNamedChild("size"), fixed.getSizeElement(), "size", focus); checkFixedValue(errors, path + ".hash", focus.getNamedChild("hash"), fixed.getHashElement(), "hash", focus); checkFixedValue(errors, path + ".title", focus.getNamedChild("title"), fixed.getTitleElement(), "title", focus); } // public API private boolean checkCode(List<ValidationMessage> errors, Element element, String path, String code, String system, String display) 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(system, code, display); txTime = txTime + (System.nanoTime() - t); if (s == null || s.isOk()) return true; if (s.getSeverity() == IssueSeverity.INFORMATION) hint(errors, IssueType.CODEINVALID, element.line(), element.col(), path, s == null, s.getMessage()); else if (s.getSeverity() == IssueSeverity.WARNING) warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, s == null, s.getMessage()); else return rule(errors, IssueType.CODEINVALID, element.line(), element.col(), path, s == null, s.getMessage()); return true; } else if (system.startsWith("http://hl7.org/fhir")) { if (system.equals("http://hl7.org/fhir/sid/icd-10")) return true; // else don't check ICD-10 (for now) else { CodeSystem cs = getCodeSystem(system); if (warning(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 (system.startsWith("http://loinc.org")) { return true; } else if (system.startsWith("http://unitsofmeasure.org")) { return true; } else return true; } private void checkCodeableConcept(List<ValidationMessage> errors, String path, Element focus, CodeableConcept fixed) { checkFixedValue(errors, path + ".text", focus.getNamedChild("text"), fixed.getTextElement(), "text", focus); List<Element> codings = new ArrayList<Element>(); focus.getNamedChildren("coding", codings); 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), "coding", focus); } } private void checkCodeableConcept(List<ValidationMessage> errors, String path, Element element, StructureDefinition profile, ElementDefinition theElementCntext) { 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() && binding.getValueSet() instanceof Reference) { 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")) { try { CodeableConcept cc = 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) 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(cc, valueset); if (!vr.isOk()) { bindingsOk = false; if (vr.getErrorClass() != null && vr.getErrorClass().isInfrastructure()) { if (binding.getStrength() == BindingStrength.REQUIRED) warning(errors, 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, (Reference) binding.getExtensionsByUrl("http://hl7.org/fhir/StructureDefinition/elementdefinition-maxValueSet").get(0).getValue(), cc); else warning(errors, 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) hint(errors, 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) rule(errors, 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, (Reference) binding.getExtensionsByUrl("http://hl7.org/fhir/StructureDefinition/elementdefinition-maxValueSet").get(0).getValue(), cc); else warning(errors, 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) hint(errors, 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) warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, vr.getMessage()); } // 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(nextSystem, nextCode, null); if (!vr.isOk()) { warning(errors, 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"); } } } 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"); } } } } private void checkMaxValueSet(List<ValidationMessage> errors, String path, Element element, StructureDefinition profile, Reference maxVSUrl, CodeableConcept cc) { // 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")) { try { long t = System.nanoTime(); ValidationResult vr = context.validateCode(cc, valueset); txTime = txTime + (System.nanoTime() - t); if (!vr.isOk()) { rule(errors, 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<ValidationMessage> errors, String path, Element element, StructureDefinition profile, Reference maxVSUrl, Coding c) { // 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")) { try { long t = System.nanoTime(); ValidationResult vr = context.validateCode(c, valueset); txTime = txTime + (System.nanoTime() - t); if (!vr.isOk()) { rule(errors, 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<ValidationMessage> errors, String path, Element element, StructureDefinition profile, Reference maxVSUrl, String value) { // 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")) { try { long t = System.nanoTime(); ValidationResult vr = context.validateCode(null, value, null, valueset); txTime = txTime + (System.nanoTime() - t); if (!vr.isOk()) { if (vr.getErrorClass() != null && vr.getErrorClass().isInfrastructure()) warning(errors, 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 rule(errors, 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 CodeableConcept readAsCodeableConcept(Element element) { CodeableConcept cc = new CodeableConcept(); List<Element> list = new ArrayList<Element>(); element.getNamedChildren("coding", list); for (Element item : list) cc.addCoding(readAsCoding(item)); cc.setText(element.getNamedChildValue("text")); return cc; } private Coding readAsCoding(Element item) { Coding c = new Coding(); c.setSystem(item.getNamedChildValue("system")); c.setVersion(item.getNamedChildValue("version")); c.setCode(item.getNamedChildValue("code")); c.setDisplay(item.getNamedChildValue("display")); return c; } private void checkCoding(List<ValidationMessage> errors, String path, Element focus, Coding fixed) { checkFixedValue(errors, path + ".system", focus.getNamedChild("system"), fixed.getSystemElement(), "system", focus); checkFixedValue(errors, path + ".code", focus.getNamedChild("code"), fixed.getCodeElement(), "code", focus); checkFixedValue(errors, path + ".display", focus.getNamedChild("display"), fixed.getDisplayElement(), "display", focus); checkFixedValue(errors, path + ".userSelected", focus.getNamedChild("userSelected"), fixed.getUserSelectedElement(), "userSelected", focus); } private void checkCoding(List<ValidationMessage> errors, String path, Element element, StructureDefinition profile, ElementDefinition theElementCntext, boolean inCodeableConcept) { 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)) 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() && binding.getValueSet() instanceof Reference) { 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")) { try { Coding c = readAsCoding(element); long t = System.nanoTime(); ValidationResult vr = context.validateCode(c, valueset); txTime = txTime + (System.nanoTime() - t); if (!vr.isOk()) { if (vr.IsNoService()) hint(errors, 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) warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "Could not confirm that the codes provided are in the value set " + describeReference(binding.getValueSet()) + " (" + valueset.getUrl()+", 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, (Reference) binding.getExtensionsByUrl("http://hl7.org/fhir/StructureDefinition/elementdefinition-maxValueSet").get(0).getValue(), c); else warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "Could not confirm that 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)"); } else if (binding.getStrength() == BindingStrength.PREFERRED) hint(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "Could not confirm that the codes provided are in the value set " + describeReference(binding.getValueSet()) + " (" + valueset.getUrl() + ", and a code is recommended to come from this value set)"); } else if (binding.getStrength() == BindingStrength.REQUIRED) rule(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "The Coding provided is not in the value set " + describeReference(binding.getValueSet()) + " (" + valueset.getUrl() + ", 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, (Reference) binding.getExtensionsByUrl("http://hl7.org/fhir/StructureDefinition/elementdefinition-maxValueSet").get(0).getValue(), c); else warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "The Coding provided is not in the value set " + describeReference(binding.getValueSet()) + " (" + valueset.getUrl() + ", 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) hint(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "The Coding provided is not in the value set " + describeReference(binding.getValueSet()) + " (" + valueset.getUrl() + ", 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"); } } } 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<ValidationMessage> errors, String path, Element focus, ContactPoint fixed) { checkFixedValue(errors, path + ".system", focus.getNamedChild("system"), fixed.getSystemElement(), "system", focus); checkFixedValue(errors, path + ".value", focus.getNamedChild("value"), fixed.getValueElement(), "value", focus); checkFixedValue(errors, path + ".use", focus.getNamedChild("use"), fixed.getUseElement(), "use", focus); checkFixedValue(errors, path + ".period", focus.getNamedChild("period"), fixed.getPeriod(), "period", focus); } protected void checkDeclaredProfiles(ResourceProfiles resourceProfiles, List<ValidationMessage> errors, Element resource, Element element, NodeStack stack) throws FHIRException { Element meta = element.getNamedChild("meta"); if (meta != null) { List<Element> profiles = new ArrayList<Element>(); meta.getNamedChildren("profile", profiles); int i = 0; for (Element profile : profiles) { String ref = profile.primitiveValue(); String p = stack.addToLiteralPath("meta", "profile", ":" + Integer.toString(i)); if (rule(errors, IssueType.INVALID, element.line(), element.col(), p, !Utilities.noString(ref), "StructureDefinition reference invalid")) { long t = System.nanoTime(); resourceProfiles.addProfile(errors, ref, errorForUnknownProfiles, p, element); i++; } } } } private StructureDefinition checkExtension(Object appContext, List<ValidationMessage> errors, String path, Element resource, Element element, ElementDefinition def, StructureDefinition profile, NodeStack stack) throws FHIRException, IOException { String url = element.getNamedChildValue("url"); boolean isModifier = element.getName().equals("modifierExtension"); long t = System.nanoTime(); StructureDefinition ex = context.fetchResource(StructureDefinition.class, url); sdTime = sdTime + (System.nanoTime() - t); if (ex == null) { 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); } else { 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, element, /* path+"[url='"+url+"']", */ ex, stack, ex.getUrl()); 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<String> allowedTypes = listExtensionTypes(ex); String actualType = getExtensionType(element); if (actualType == null) rule(errors, IssueType.STRUCTURE, element.line(), element.col(), path + "[url='" + url + "']", allowedTypes.isEmpty(), "The Extension '" + url + "' definition is for a complex extension, so it cannot contain a value"); else rule(errors, IssueType.STRUCTURE, element.line(), element.col(), path + "[url='" + url + "']", 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(appContext, errors, ex, ex.getSnapshot().getElement().get(0), null, null, resource, element, "Extension", stack, false); } return ex; } 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; } } return null; } private Set<String> listExtensionTypes(StructureDefinition ex) { ElementDefinition vd = null; for (ElementDefinition ed : ex.getSnapshot().getElement()) { if (ed.getPath().startsWith("Extension.value")) { vd = ed; break; } } Set<String> res = new HashSet<String>(); if (vd != null && !"0".equals(vd.getMax())) { for (TypeRefComponent tr : vd.getType()) { res.add(tr.getCode()); } } return res; } private boolean checkExtensionContext(List<ValidationMessage> errors, Element element, StructureDefinition definition, NodeStack stack, String extensionParent) { String extUrl = definition.getUrl(); CommaSeparatedStringBuilder p = new CommaSeparatedStringBuilder(); for (String lp : stack.getLogicalPaths()) p.append(lp); if (definition.getContextType() == ExtensionContext.DATATYPE) { boolean ok = false; CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder(); for (StringType ct : definition.getContext()) { b.append(ct.getValue()); if (ct.getValue().equals("*") || stack.getLogicalPaths().contains(ct.getValue() + ".extension")) ok = true; } return rule(errors, IssueType.STRUCTURE, element.line(), element.col(), stack.getLiteralPath(), ok, "The extension " + extUrl + " is not allowed to be used on the logical path set [" + p.toString() + "] (allowed: datatype=" + b.toString() + ")"); } else if (definition.getContextType() == ExtensionContext.EXTENSION) { boolean ok = false; for (StringType ct : definition.getContext()) if (ct.getValue().equals("*") || ct.getValue().equals(extensionParent)) ok = true; return rule(errors, IssueType.STRUCTURE, element.line(), element.col(), stack.getLiteralPath(), ok, "The extension " + extUrl + " is not allowed to be used with the extension '" + extensionParent + "'"); } else if (definition.getContextType() == ExtensionContext.RESOURCE) { boolean ok = false; // String simplePath = container.getPath(); // System.out.println(simplePath); // if (effetive.endsWith(".extension") || simplePath.endsWith(".modifierExtension")) // simplePath = simplePath.substring(0, simplePath.lastIndexOf('.')); CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder(); for (StringType ct : definition.getContext()) { String c = ct.getValue(); b.append(c); if (c.equals("*") || stack.getLogicalPaths().contains(c + ".extension") || (c.startsWith("@") && stack.getLogicalPaths().contains(c.substring(1) + ".extension"))) ; ok = true; } return rule(errors, IssueType.STRUCTURE, element.line(), element.col(), stack.getLiteralPath(), ok, "The extension " + extUrl + " is not allowed to be used on the logical path set " + p.toString() + " (allowed: resource=" + b.toString() + ")"); } else throw new Error("Unknown context type"); } // // private String simplifyPath(String path) { // String s = path.replace("/f:", "."); // while (s.contains("[")) // s = s.substring(0, s.indexOf("["))+s.substring(s.indexOf("]")+1); // String[] parts = s.split("\\."); // int i = 0; // while (i < parts.length && !context.getProfiles().containsKey(parts[i].toLowerCase())) // i++; // if (i >= parts.length) // throw new Error("Unable to process part "+path); // int j = parts.length - 1; // while (j > 0 && (parts[j].equals("extension") || parts[j].equals("modifierExtension"))) // j--; // StringBuilder b = new StringBuilder(); // boolean first = true; // for (int k = i; k <= j; k++) { // if (k == j || !parts[k].equals(parts[k+1])) { // if (first) // first = false; // else // b.append("."); // b.append(parts[k]); // } // } // return b.toString(); // } // private void checkFixedValue(List<ValidationMessage> errors, String path, Element focus, org.hl7.fhir.dstu3.model.Element fixed, String propName, Element parent) { if ((fixed == null || fixed.isEmpty()) && focus == null) ; // this is all good else if (fixed == null && focus != null) rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, false, "Unexpected element " + focus.getName()); 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+"'"); else { String value = focus.primitiveValue(); if (fixed instanceof org.hl7.fhir.dstu3.model.BooleanType) rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.dstu3.model.BooleanType) fixed).asStringValue(), value), "Value is '" + value + "' but must be '" + ((org.hl7.fhir.dstu3.model.BooleanType) fixed).asStringValue() + "'"); else if (fixed instanceof org.hl7.fhir.dstu3.model.IntegerType) rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.dstu3.model.IntegerType) fixed).asStringValue(), value), "Value is '" + value + "' but must be '" + ((org.hl7.fhir.dstu3.model.IntegerType) fixed).asStringValue() + "'"); else if (fixed instanceof org.hl7.fhir.dstu3.model.DecimalType) rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.dstu3.model.DecimalType) fixed).asStringValue(), value), "Value is '" + value + "' but must be '" + ((org.hl7.fhir.dstu3.model.DecimalType) fixed).asStringValue() + "'"); else if (fixed instanceof org.hl7.fhir.dstu3.model.Base64BinaryType) rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.dstu3.model.Base64BinaryType) fixed).asStringValue(), value), "Value is '" + value + "' but must be '" + ((org.hl7.fhir.dstu3.model.Base64BinaryType) fixed).asStringValue() + "'"); else if (fixed instanceof org.hl7.fhir.dstu3.model.InstantType) rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.dstu3.model.InstantType) fixed).getValue().toString(), value), "Value is '" + value + "' but must be '" + ((org.hl7.fhir.dstu3.model.InstantType) fixed).asStringValue() + "'"); else if (fixed instanceof org.hl7.fhir.dstu3.model.StringType) rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.dstu3.model.StringType) fixed).getValue(), value), "Value is '" + value + "' but must be '" + ((org.hl7.fhir.dstu3.model.StringType) fixed).getValue() + "'"); else if (fixed instanceof org.hl7.fhir.dstu3.model.UriType) rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.dstu3.model.UriType) fixed).getValue(), value), "Value is '" + value + "' but must be '" + ((org.hl7.fhir.dstu3.model.UriType) fixed).getValue() + "'"); else if (fixed instanceof org.hl7.fhir.dstu3.model.DateType) rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.dstu3.model.DateType) fixed).getValue().toString(), value), "Value is '" + value + "' but must be '" + ((org.hl7.fhir.dstu3.model.DateType) fixed).getValue() + "'"); else if (fixed instanceof org.hl7.fhir.dstu3.model.DateTimeType) rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.dstu3.model.DateTimeType) fixed).getValue().toString(), value), "Value is '" + value + "' but must be '" + ((org.hl7.fhir.dstu3.model.DateTimeType) fixed).getValue() + "'"); else if (fixed instanceof org.hl7.fhir.dstu3.model.OidType) rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.dstu3.model.OidType) fixed).getValue(), value), "Value is '" + value + "' but must be '" + ((org.hl7.fhir.dstu3.model.OidType) fixed).getValue() + "'"); else if (fixed instanceof org.hl7.fhir.dstu3.model.UuidType) rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.dstu3.model.UuidType) fixed).getValue(), value), "Value is '" + value + "' but must be '" + ((org.hl7.fhir.dstu3.model.UuidType) fixed).getValue() + "'"); else if (fixed instanceof org.hl7.fhir.dstu3.model.CodeType) rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.dstu3.model.CodeType) fixed).getValue(), value), "Value is '" + value + "' but must be '" + ((org.hl7.fhir.dstu3.model.CodeType) fixed).getValue() + "'"); else if (fixed instanceof org.hl7.fhir.dstu3.model.IdType) rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.dstu3.model.IdType) fixed).getValue(), value), "Value is '" + value + "' but must be '" + ((org.hl7.fhir.dstu3.model.IdType) fixed).getValue() + "'"); else if (fixed instanceof Quantity) checkQuantity(errors, path, focus, (Quantity) fixed); else if (fixed instanceof Address) checkAddress(errors, path, focus, (Address) fixed); else if (fixed instanceof ContactPoint) checkContactPoint(errors, path, focus, (ContactPoint) fixed); else if (fixed instanceof Attachment) checkAttachment(errors, path, focus, (Attachment) fixed); else if (fixed instanceof Identifier) checkIdentifier(errors, path, focus, (Identifier) fixed); else if (fixed instanceof Coding) checkCoding(errors, path, focus, (Coding) fixed); else if (fixed instanceof HumanName) checkHumanName(errors, path, focus, (HumanName) fixed); else if (fixed instanceof CodeableConcept) checkCodeableConcept(errors, path, focus, (CodeableConcept) fixed); else if (fixed instanceof Timing) checkTiming(errors, path, focus, (Timing) fixed); else if (fixed instanceof Period) checkPeriod(errors, path, focus, (Period) fixed); else if (fixed instanceof Range) checkRange(errors, path, focus, (Range) fixed); else if (fixed instanceof Ratio) checkRatio(errors, path, focus, (Ratio) fixed); else if (fixed instanceof SampledData) checkSampledData(errors, path, focus, (SampledData) fixed); else rule(errors, IssueType.EXCEPTION, focus.line(), focus.col(), path, false, "Unhandled fixed value type " + fixed.getClass().getName()); List<Element> extensions = new ArrayList<Element>(); focus.getNamedChildren("extension", extensions); if (fixed.getExtension().size() == 0) { rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, extensions.size() == 0, "No extensions allowed"); } 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(), "extension.value", ex.getNamedChild("extension")); } } } } } private void checkHumanName(List<ValidationMessage> errors, String path, Element focus, HumanName fixed) { checkFixedValue(errors, path + ".use", focus.getNamedChild("use"), fixed.getUseElement(), "use", focus); checkFixedValue(errors, path + ".text", focus.getNamedChild("text"), fixed.getTextElement(), "text", focus); checkFixedValue(errors, path + ".period", focus.getNamedChild("period"), fixed.getPeriod(), "period", focus); List<Element> parts = new ArrayList<Element>(); 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(), "family", focus); } 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), "given", focus); } 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), "prefix", focus); } 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), "suffix", focus); } } private void checkIdentifier(List<ValidationMessage> 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"); } private void checkIdentifier(List<ValidationMessage> errors, String path, Element focus, Identifier fixed) { checkFixedValue(errors, path + ".use", focus.getNamedChild("use"), fixed.getUseElement(), "use", focus); checkFixedValue(errors, path + ".type", focus.getNamedChild("type"), fixed.getType(), "type", focus); checkFixedValue(errors, path + ".system", focus.getNamedChild("system"), fixed.getSystemElement(), "system", focus); checkFixedValue(errors, path + ".value", focus.getNamedChild("value"), fixed.getValueElement(), "value", focus); checkFixedValue(errors, path + ".period", focus.getNamedChild("period"), fixed.getPeriod(), "period", focus); checkFixedValue(errors, path + ".assigner", focus.getNamedChild("assigner"), fixed.getAssigner(), "assigner", focus); } private void checkPeriod(List<ValidationMessage> errors, String path, Element focus, Period fixed) { checkFixedValue(errors, path + ".start", focus.getNamedChild("start"), fixed.getStartElement(), "start", focus); checkFixedValue(errors, path + ".end", focus.getNamedChild("end"), fixed.getEndElement(), "end", focus); } private void checkPrimitive(Object appContext, List<ValidationMessage> errors, String path, String type, ElementDefinition context, Element e, StructureDefinition profile) throws FHIRException, IOException { if (isBlank(e.primitiveValue())) { rule(errors, IssueType.INVALID, e.line(), e.col(), path, e.hasChildren(), "primitive types must have a value or must have child extensions"); 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 + "'"); 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")) { rule(errors, IssueType.INVALID, e.line(), e.col(), path, !e.primitiveValue().startsWith("oid:"), "URI values cannot start with oid:"); rule(errors, IssueType.INVALID, e.line(), e.col(), path, !e.primitiveValue().startsWith("uuid:"), "URI values cannot start with uuid:"); rule(errors, IssueType.INVALID, e.line(), e.col(), path, e.primitiveValue().equals(e.primitiveValue().trim()), "URI values cannot have leading or trailing whitespace"); 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()); // now, do we check the URI target? if (fetcher != null) { rule(errors, IssueType.INVALID, e.line(), e.col(), path, fetcher.resolveURL(appContext, path, e.primitiveValue()), "URL value '"+e.primitiveValue()+"' does not resolve"); } } 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"); 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")) { rule(errors, IssueType.INVALID, e.line(), e.col(), path, yearIsValid(e.primitiveValue()), "The value '" + e.primitiveValue() + "' does not have a valid year"); rule(errors, IssueType.INVALID, e.line(), e.col(), path, e.primitiveValue() .matches("-?[0-9]{4}(-(0[1-9]|1[0-2])(-(0[0-9]|[1-2][0-9]|3[0-1])(T([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9](\\.[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()); } if (type.equals("date")) { rule(errors, IssueType.INVALID, e.line(), e.col(), path, yearIsValid(e.primitiveValue()), "The value '" + e.primitiveValue() + "' does not have a valid year"); rule(errors, IssueType.INVALID, e.line(), e.col(), path, e.primitiveValue().matches("-?[0-9]{4}(-(0[1-9]|1[0-2])(-(0[0-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()); } if (type.equals("integer")) { rule(errors, IssueType.INVALID, e.line(), e.col(), path, !context.hasMaxValueIntegerType() || !context.getMaxValueIntegerType().hasValue() || (context.getMaxValueIntegerType().getValue() >= new Integer(e.getValue()).intValue()), "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() <= new Integer(e.getValue()).intValue()), "value is less than permitted minimum value of " + (context.hasMinValueIntegerType() ? context.getMinValueIntegerType() : "")); } 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[0-9]|[1-2][0-9]|3[0-1])T([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9](\\.[0-9]+)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00))"), "The instant '" + e.primitiveValue() + "' is not valid (by regex)"); rule(errors, IssueType.INVALID, e.line(), e.col(), path, yearIsValid(e.primitiveValue()), "The value '" + e.primitiveValue() + "' does not have a valid year"); } 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); } 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+"')"); // 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(), context.getSliceName(), null); // for nothing to check } private void checkInnerNames(List<ValidationMessage> errors, Element e, String path, List<XhtmlNode> 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" ), "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" ); 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()); } } } private void checkInnerNS(List<ValidationMessage> errors, Element e, String path, List<XhtmlNode> 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+"')"); checkInnerNS(errors, e, path, node.getChildNodes()); } } } private void checkPrimitiveBinding(List<ValidationMessage> errors, String path, String type, ElementDefinition elementContext, Element element, StructureDefinition profile) { // We ignore bindings that aren't on string, uri or code if (!element.hasPrimitiveValue() || !("code".equals(type) || "string".equals(type) || "uri".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() && binding.getValueSet() instanceof Reference) { ValueSet vs = resolveBindingReference(profile, binding.getValueSet(), profile.getUrl()); if (warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, vs != null, "ValueSet {0} not found", describeReference(binding.getValueSet()))) { long t = System.nanoTime(); ValidationResult vr = context.validateCode(null, value, null, vs); txTime = txTime + (System.nanoTime() - t); if (vr != null && !vr.isOk()) { if (vr.IsNoService()) hint(errors, 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) rule(errors, 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)"+(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, (Reference) binding.getExtensionsByUrl("http://hl7.org/fhir/StructureDefinition/elementdefinition-maxValueSet").get(0).getValue(), value); else warning(errors, 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)"+(vr.getMessage() != null ? " (error message = "+vr.getMessage()+")" : "")); } else if (binding.getStrength() == BindingStrength.PREFERRED) hint(errors, 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)"+(vr.getMessage() != null ? " (error message = "+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"); } private void checkQuantity(List<ValidationMessage> errors, String path, Element focus, Quantity fixed) { checkFixedValue(errors, path + ".value", focus.getNamedChild("value"), fixed.getValueElement(), "value", focus); checkFixedValue(errors, path + ".comparator", focus.getNamedChild("comparator"), fixed.getComparatorElement(), "comparator", focus); checkFixedValue(errors, path + ".units", focus.getNamedChild("unit"), fixed.getUnitElement(), "units", focus); checkFixedValue(errors, path + ".system", focus.getNamedChild("system"), fixed.getSystemElement(), "system", focus); checkFixedValue(errors, path + ".code", focus.getNamedChild("code"), fixed.getCodeElement(), "code", focus); } // implementation private void checkRange(List<ValidationMessage> errors, String path, Element focus, Range fixed) { checkFixedValue(errors, path + ".low", focus.getNamedChild("low"), fixed.getLow(), "low", focus); checkFixedValue(errors, path + ".high", focus.getNamedChild("high"), fixed.getHigh(), "high", focus); } private void checkRatio(List<ValidationMessage> errors, String path, Element focus, Ratio fixed) { checkFixedValue(errors, path + ".numerator", focus.getNamedChild("numerator"), fixed.getNumerator(), "numerator", focus); checkFixedValue(errors, path + ".denominator", focus.getNamedChild("denominator"), fixed.getDenominator(), "denominator", focus); } private void checkReference(Object appContext, List<ValidationMessage> errors, String path, Element element, StructureDefinition profile, ElementDefinition container, String parentType, NodeStack stack) throws FHIRException, IOException { String ref = null; try { // Do this inside a try because invalid instances might provide more than one reference. ref = element.getNamedChildValue("reference"); } catch (Error e) { } if (Utilities.noString(ref)) { // todo - what should we do in this case? warning(errors, IssueType.STRUCTURE, element.line(), element.col(), path, !Utilities.noString(element.getNamedChildValue("display")), "A Reference without an actual reference should have a display"); return; } Element we = localResolve(ref, stack, errors, path); String refType; if (ref.startsWith("#")) { refType = "contained"; } else { if (we == null) { refType = "remote"; } else { refType = "bundle"; } } String ft; if (we != null) ft = we.getType(); else ft = tryParse(ref); ReferenceValidationPolicy pol = refType.equals("contained") ? ReferenceValidationPolicy.CHECK_VALID : fetcher == null ? ReferenceValidationPolicy.IGNORE : fetcher.validationPolicy(appContext, path, ref); if (pol.checkExists()) { if (we == null) { if (fetcher == null) throw new FHIRException("Resource resolution services not provided"); we = fetcher.fetch(appContext, ref); } rule(errors, IssueType.STRUCTURE, element.line(), element.col(), path, we != null, "Unable to resolve resource '"+ref+"'"); } if (we != null && pol.checkType()) { if (warning(errors, IssueType.STRUCTURE, element.line(), element.col(), path, ft!=null, "Unable to determine type of target resource")) { boolean ok = false; CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder(); for (TypeRefComponent type : container.getType()) { if (!ok && type.getCode().equals("Reference")) { // we validate as much as we can. First, can we infer a type from the profile? if (!type.hasTargetProfile() || type.getTargetProfile().equals("http://hl7.org/fhir/StructureDefinition/Resource")) ok = true; else { String pr = type.getTargetProfile(); String bt = getBaseType(profile, pr); StructureDefinition sd = context.fetchResource(StructureDefinition.class, "http://hl7.org/fhir/StructureDefinition/" + bt); if (rule(errors, IssueType.STRUCTURE, element.line(), element.col(), path, bt != null, "Unable to resolve the profile reference '" + pr + "'")) { b.append(bt); ok = bt.equals(ft); if (ok && we!=null && pol.checkValid()) { doResourceProfile(appContext, we, pr, errors, stack.push(we, -1, null, null), path, element); } } else ok = true; // suppress following check if (ok && type.hasAggregation()) { boolean modeOk; for (Enumeration<AggregationMode> mode : type.getAggregation()) { if (mode.getValue().equals(AggregationMode.CONTAINED) && refType.equals("contained")) ok = true; else if (mode.getValue().equals(AggregationMode.BUNDLED) && refType.equals("bundled")) ok = true; else if (mode.getValue().equals(AggregationMode.REFERENCED) && (refType.equals("bundled")||refType.equals("remote"))) ok = true; } rule(errors, IssueType.STRUCTURE, element.line(), element.col(), path, ok, "Reference is " + refType + " which isn't supported by the specified aggregation mode(s) for the reference"); } } } if (!ok && type.getCode().equals("*")) { ok = true; // can refer to anything } } rule(errors, IssueType.STRUCTURE, element.line(), element.col(), path, ok, "Invalid Resource target type. Found " + ft + ", but expected one of (" + b.toString() + ")"); } } if (pol == ReferenceValidationPolicy.CHECK_VALID) { // todo.... } } private void doResourceProfile(Object appContext, Element resource, String profile, List<ValidationMessage> errors, NodeStack stack, String path, Element element) throws FHIRException, IOException { ResourceProfiles resourceProfiles = addResourceProfile(errors, resource, profile, path, element, stack); if (resourceProfiles.isProcessed()) { start(appContext, errors, resource, resource, null, stack); } } private ResourceProfiles getResourceProfiles(Element resource, NodeStack stack) { ResourceProfiles resourceProfiles = resourceProfilesMap.get(resource); if (resourceProfiles==null) { resourceProfiles = new ResourceProfiles(resource, stack); resourceProfilesMap.put(resource, resourceProfiles); } return resourceProfiles; } private ResourceProfiles addResourceProfile(List<ValidationMessage> errors, Element resource, String profile, String path, Element element, NodeStack stack) { ResourceProfiles resourceProfiles = getResourceProfiles(resource, stack); resourceProfiles.addProfile(errors, profile, errorForUnknownProfiles, path, element); return resourceProfiles; } 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<ValidationMessage> errors, String path, Element focus, SampledData fixed) { checkFixedValue(errors, path + ".origin", focus.getNamedChild("origin"), fixed.getOrigin(), "origin", focus); checkFixedValue(errors, path + ".period", focus.getNamedChild("period"), fixed.getPeriodElement(), "period", focus); checkFixedValue(errors, path + ".factor", focus.getNamedChild("factor"), fixed.getFactorElement(), "factor", focus); checkFixedValue(errors, path + ".lowerLimit", focus.getNamedChild("lowerLimit"), fixed.getLowerLimitElement(), "lowerLimit", focus); checkFixedValue(errors, path + ".upperLimit", focus.getNamedChild("upperLimit"), fixed.getUpperLimitElement(), "upperLimit", focus); checkFixedValue(errors, path + ".dimensions", focus.getNamedChild("dimensions"), fixed.getDimensionsElement(), "dimensions", focus); checkFixedValue(errors, path + ".data", focus.getNamedChild("data"), fixed.getDataElement(), "data", focus); } private void checkTiming(List<ValidationMessage> errors, String path, Element focus, Timing fixed) { checkFixedValue(errors, path + ".repeat", focus.getNamedChild("repeat"), fixed.getRepeat(), "value", focus); List<Element> events = new ArrayList<Element>(); 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), "event", focus); } } 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(Type reference) { if (reference == null) return "null"; if (reference instanceof UriType) return ((UriType) reference).getValue(); if (reference instanceof Reference) return ((Reference) reference).getReference(); return "??"; } private String describeTypes(List<TypeRefComponent> types) { CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder(); for (TypeRefComponent t : types) { b.append(t.getCode()); } 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 getBasePracticeWarningLevel() { return bpWarnings; } private String getBaseType(StructureDefinition profile, String pr) { StructureDefinition p = resolveProfile(profile, pr); if (p == null) return null; else return p.getType(); } @Override public CheckDisplayOption getCheckDisplay() { return checkDisplay; } // private String findProfileTag(Element element) { // String uri = null; // List<Element> list = new ArrayList<Element>(); // element.getNamedChildren("category", list); // for (Element c : list) { // if ("http://hl7.org/fhir/tag/profile".equals(c.getAttribute("scheme"))) { // uri = c.getAttribute("term"); // } // } // return uri; // } 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 Element getContainedById(Element container, String id) { List<Element> contained = new ArrayList<Element>(); container.getNamedChildren("contained", contained); for (Element we : contained) { if (id.equals(we.getNamedChildValue("id"))) return we; } return null; } public IWorkerContext getContext() { return context; } private ElementDefinition getCriteriaForDiscriminator(String path, ElementDefinition element, String discriminator, StructureDefinition profile) throws DefinitionException { StructureDefinition sd = profile; ElementDefinition ed = element; if ("value".equals(discriminator) && element.hasFixed()) return element; List<String> nodes = new ArrayList<String>(); Matcher matcher = Pattern.compile("([a-zA-Z0-0]+(\\([^\\(^\\)]*\\))?)(\\.([a-zA-Z0-0]+(\\([^\\(^\\)]*\\))?))*").matcher(discriminator); while (matcher.find()) { if (!matcher.group(1).startsWith("@")) nodes.add(matcher.group(1)); if (matcher.groupCount()>4 && matcher.group(4)!= null && !matcher.group(4).startsWith("@")) nodes.add(matcher.group(4)); } for (String fullnode : nodes) { String node = fullnode.contains("(") ? fullnode.substring(0, fullnode.indexOf('(')) : fullnode; String qualifier = fullnode.contains("(") ? fullnode.substring(fullnode.indexOf('(') + 2, fullnode.length()-2) : null; if (qualifier!=null && !node.equals("extension")) throw new DefinitionException("Function specified other than 'extension()': " + discriminator); // get the children of element List<ElementDefinition> childDefinitions; childDefinitions = ProfileUtilities.getChildMap(sd, ed); // if that's empty, get the children of the type if (childDefinitions.isEmpty()) { if (ed.getType().size() == 0) throw new DefinitionException("Error in profile for " + path + " no children, no type"); if (ed.getType().size() > 1) throw new DefinitionException("Error in profile for " + path + " multiple types defined in slice discriminator"); if (ed.hasSlicing()) { List<ElementDefinition> slices = ProfileUtilities.getSliceList(profile, ed); boolean found = false; for (ElementDefinition sed: slices) { childDefinitions = ProfileUtilities.getChildMap(sd, sed); for (ElementDefinition t : childDefinitions) { if (tailMatches(t, node)) { found = true; if (qualifier!=null) { List<ElementDefinition> childNodes = ProfileUtilities.getChildList(profile, t); if (childNodes.isEmpty()) { if (t.getType().size()==1 && t.getType().get(0).hasProfile() && t.getType().get(0).getProfile().equals(qualifier)) { found = true; break; } } else { for (ElementDefinition c : childNodes) { if (c.getPath().endsWith(".url") && c.hasFixed() && c.getFixed() instanceof UriType && ((UriType)c.getFixed()).equals(qualifier)) { found = true; break; } } } } if (found) { ed = t; break; } } } } if (found) continue; } if (ed.getType().get(0).hasProfile() && ed.getType().get(0).getCode().equals("Reference") && node.equals("reference")) { long t1 = System.nanoTime(); sd = context.fetchResource(StructureDefinition.class, ed.getType().get(0).getTargetProfile()); sdTime = sdTime + (System.nanoTime() - t1); // we've skipped the reference. ed = sd.getSnapshot().getElementFirstRep(); continue; } else if (ed.getType().get(0).hasProfile()) { long t2 = System.nanoTime(); sd = context.fetchResource(StructureDefinition.class, ed.getType().get(0).getProfile()); sdTime = sdTime + (System.nanoTime() - t2); } else { long t2 = System.nanoTime(); sd = context.fetchResource(StructureDefinition.class, "http://hl7.org/fhir/StructureDefinition/" + ed.getType().get(0).getCode()); sdTime = sdTime + (System.nanoTime() - t2); } if (sd == null) throw new DefinitionException("Error in profile for " + path + ": unable to find type for "+ed.getId()); childDefinitions = ProfileUtilities.getChildMap(sd, sd.getSnapshot().getElementFirstRep()); } // find a single match. (no slicing!) boolean found = false; for (ElementDefinition t : childDefinitions) { if (tailMatches(t, node)) { found = true; if (qualifier!=null) { found = false; List<ElementDefinition> childNodes = ProfileUtilities.getChildList(profile, t); if (childNodes.isEmpty()) { if (t.getType().size()==1 && t.getType().get(0).hasProfile() && t.getType().get(0).getProfile().equals(qualifier)) { found = true; ed = t; break; } } else { for (ElementDefinition c : childNodes) { if (c.getPath().endsWith(".url") && c.hasFixed() && c.getFixed() instanceof UriType && ((UriType)c.getFixed()).equals(qualifier)) { found = true; ed = t; break; } } } } if (found) { ed = t; break; } } } if (!found) throw new DefinitionException("Unable to find definition for discriminator "+discriminator+" from "+path+" in "+profile.getUrl()); } return ed; // List<ElementDefinition> snapshot = null; // int index; // if (childDefinitions.isEmpty()) { // // going to look at the type // if (ed.getType().size() == 0) // throw new DefinitionException("Error in profile for " + path + " no children, no type"); // snapshot = type.getSnapshot().getElement(); // ed = snapshot.get(0); // index = 0; // } else { // snapshot = childDefinitions; // index = -1; // } // String originalPath = ed.getPath(); // String goal = originalPath + "." + discriminator; // // index++; // while (index < snapshot.size() && !snapshot.get(index).getPath().equals(originalPath)) { // if (snapshot.get(index).getPath().equals(goal)) // return snapshot.get(index); // index++; // } // throw new Error("Unable to find discriminator definition for " + goal + " in " + discriminator + " at " + path+" in "+profile.getUrl()); } private boolean tailMatches(ElementDefinition t, String d) { String tail = tail(t.getPath()); if (d.contains("[")) return tail.startsWith(d.substring(0, d.indexOf('['))); else return tail.equals(d); } private Element getExtensionByUrl(List<Element> extensions, String urlSimple) { for (Element e : extensions) { if (urlSimple.equals(e.getNamedChildValue("url"))) return e; } return null; } public List<String> getExtensionDomains() { return extensionDomains; } private Element getFromBundle(Element bundle, String ref, String fullUrl, List<ValidationMessage> errors, String path) { String targetUrl = null; String version = ""; 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 rule(errors, IssueType.REQUIRED, -1, -1, path, path.startsWith("Bundle.signature"), "Relative Reference appears inside Bundle whose entry is missing a fullUrl"); return null; } else if (ref.split("/").length!=2) { rule(errors, IssueType.INVALID, -1, -1, path, false, "Relative URLs must be of the format [ResourceName]/[id]. 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] + ":"; } } else { String[] parts; parts = fullUrl.split("/"); for (int i=0; i < parts.length-2; i++) { base = base + parts[i] + "/"; } } String id = null; if (ref.contains("/_history/")) { version = ref.substring(ref.indexOf("/_history/") + 10); id = ref.substring(0, ref.indexOf("/history/")-1); } else if (base.startsWith("urn")) id = ref.split("/")[1]; else id = ref; targetUrl = base + id; } List<Element> entries = new ArrayList<Element>(); bundle.getNamedChildren("entry", entries); Element match = null; for (Element we : entries) { if (we.getChildValue("fullUrl").equals(targetUrl)) { 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; } else { try { if (r.getChildren("meta").get(0).getChildValue("versionId").equals(version)) { rule(errors, IssueType.FORBIDDEN, -1, -1, path, match==null, "Multiple matches in bundle for reference " + ref); match = r; } } 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 } } } } warning(errors, IssueType.REQUIRED, -1, -1, path, match!=null || !targetUrl.startsWith("urn"), "URN reference is not locally contained within the bundle " + ref); return match; } private StructureDefinition getProfileForType(String type) { if (logical != null) for (BundleEntryComponent be : logical.getEntry()) { if (be.hasResource() && be.getResource() instanceof StructureDefinition) { StructureDefinition sd = (StructureDefinition) be.getResource(); if (sd.getId().equals(type)) return sd; } } long t = System.nanoTime(); try { return context.fetchResource(StructureDefinition.class, "http://hl7.org/fhir/StructureDefinition/" + type); } finally { sdTime = sdTime + (System.nanoTime() - t); } } private Element getValueForDiscriminator(Object appContext, List<ValidationMessage> 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<Element> 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 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 isPrimitiveType(String code) { StructureDefinition sd = context.fetchResource(StructureDefinition.class, "http://hl7.org/fhir/StructureDefinition/"+code); return sd != null && sd.getKind() == StructureDefinitionKind.PRIMITIVETYPE; } 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)); else return (name.equals(tail)); } // private String mergePath(String path1, String path2) { // // path1 is xpath path // // path2 is dotted path // String[] parts = path2.split("\\."); // StringBuilder b = new StringBuilder(path1); // for (int i = 1; i < parts.length -1; i++) // b.append("/f:"+parts[i]); // return b.toString(); // } 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; } return true; } private Element localResolve(String ref, NodeStack stack, List<ValidationMessage> errors, String path) { if (ref.startsWith("#")) { // work back through the contained 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 Element res = getContainedById(stack.getElement(), ref.substring(1)); if (res != null) return res; } 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 contained 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.getElement().getName().equals("entry")) { 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(), fullUrl!=null, "Bundle entry missing fullUrl"); } if ("Bundle".equals(stack.getElement().getType())) { Element res = getFromBundle(stack.getElement(), ref, fullUrl, errors, path); return res; } stack = stack.parent; } } return null; } private Element resolve(Object appContext, String ref, NodeStack stack, List<ValidationMessage> errors, String path) throws IOException, FHIRException { Element local = localResolve(ref, stack, errors, path); if (local!=null) return local; if (fetcher == null) return null; return fetcher.fetch(appContext, ref); } private ValueSet resolveBindingReference(DomainResource ctxt, Type reference, String uri) { if (reference instanceof UriType) { long t = System.nanoTime(); ValueSet fr = context.fetchResource(ValueSet.class, ((UriType) reference).getValue().toString()); txTime = txTime + (System.nanoTime() - t); return fr; } else if (reference instanceof Reference) { String s = ((Reference) reference).getReference(); if (s.startsWith("#")) { for (Resource c : ctxt.getContained()) { if (c.getId().equals(s.substring(1)) && (c instanceof ValueSet)) return (ValueSet) c; } return null; } else { long t = System.nanoTime(); String ref = ((Reference) reference).getReference(); if (!Utilities.isAbsoluteUrl(ref)) ref = resolve(uri, ref); ValueSet fr = context.fetchResource(ValueSet.class, ref); txTime = txTime + (System.nanoTime() - t); return fr; } } else return null; } private String resolve(String uri, String 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<Element> entries, String ref, String fullUrl, String type, String id) { if (Utilities.isAbsoluteUrl(ref)) { // if the reference is absolute, then you resolve by fullUrl. No other thinking is required. for (Element entry : entries) { String fu = entry.getNamedChildValue("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((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 (u != null && fullUrl.equals(u)) return entry; if (u == null) { Element resource = entry.getNamedChild("resource"); String et = resource.getType(); String eid = resource.getNamedChildValue("id"); if (t.equals(et) && i.equals(eid)) return entry; } } } return null; } } private ElementDefinition resolveNameReference(StructureDefinitionSnapshotComponent snapshot, String contentReference) { for (ElementDefinition ed : snapshot.getElement()) if (contentReference.equals("#"+ed.getId())) 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 ElementDefinition resolveType(String type) { if (logical != null) for (BundleEntryComponent be : logical.getEntry()) { if (be.hasResource() && be.getResource() instanceof StructureDefinition) { StructureDefinition sd = (StructureDefinition) be.getResource(); if (sd.getId().equals(type)) return sd.getSnapshot().getElement().get(0); } } String url = "http://hl7.org/fhir/StructureDefinition/" + type; long t = System.nanoTime(); StructureDefinition sd = context.fetchResource(StructureDefinition.class, url); sdTime = sdTime + (System.nanoTime() - t); if (sd == null || !sd.hasSnapshot()) return null; else return sd.getSnapshot().getElement().get(0); } 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 boolean isAllowXsiLocation() { return 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 slice * - 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(Object appContext, Element element, String path, ElementDefinition slicer, ElementDefinition ed, StructureDefinition profile, List<ValidationMessage> errors, NodeStack stack) throws DefinitionException, FHIRException, IOException { 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 String expression = "true"; for (ElementDefinitionSlicingDiscriminatorComponent s : slicer.getSlicing().getDiscriminator()) { String discriminator = s.getPath(); if (s.getType() == DiscriminatorType.PROFILE) throw new FHIRException("Validating against slices with discriminators based on profiles is not yet supported by the FHIRPath engine: " + discriminator); // Todo: Fix this once FHIRPath (and this engine) supports a boolean function that test profile conformance ElementDefinition criteriaElement = getCriteriaForDiscriminator(path, ed, discriminator, profile); 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).getCode(); } if (type==null) // Slicer won't ever have a slice name, so this error needs to be reworked throw new DefinitionException("Discriminator (" + discriminator + ") is based on type, but slice " + slicer.getSliceName() + " does not declare a type"); if (discriminator.isEmpty()) expression = expression + " and this is " + type; else expression = expression + " and " + discriminator + " is " + type; } else if (criteriaElement.hasFixed()) { expression = expression + " and " + discriminator + " = "; Type fixed = criteriaElement.getFixed(); 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 = expression + "'" + escapedString + "'"; } else if (fixed instanceof UriType) { expression = expression + "'" + ((UriType)fixed).asStringValue() + "'"; } else if (fixed instanceof IntegerType) { expression = expression + ((IntegerType)fixed).asStringValue(); } else if (fixed instanceof DecimalType) { expression = expression + ((IntegerType)fixed).asStringValue(); } else if (fixed instanceof BooleanType) { expression = expression + ((BooleanType)fixed).asStringValue(); } else throw new DefinitionException("Unsupported fixed value type for discriminator(" + discriminator + ") for slice " + slicer.getSliceName() + ": " + fixed.getClass().getName()); } else if (criteriaElement.hasBinding() && criteriaElement.getBinding().hasStrength() && criteriaElement.getBinding().getStrength().equals(BindingStrength.REQUIRED) && criteriaElement.getBinding().getValueSetReference()!=null) { expression = expression + " and " + discriminator + " in '" + criteriaElement.getBinding().getValueSetReference().getReference() + "'"; } else if (criteriaElement.hasMin() && criteriaElement.getMin()>0) { expression = expression + " and " + discriminator + ".exists()"; } else if (criteriaElement.hasMax() && criteriaElement.getMax().equals("0")) { expression = expression + " and " + discriminator + ".exists().not()"; } else { throw new DefinitionException("Could not match discriminator (" + discriminator + ") for slice " + slicer.getSliceName() + " - does not have fixed value, binding or existence assertions"); } } try { n = fpe.parse(expression); } 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); } String msg; boolean ok; try { long t = System.nanoTime(); ok = fpe.evaluateToBoolean(null, element, n); fpeTime = fpeTime + (System.nanoTime() - t); msg = fpe.forLog(); } catch (Exception ex) { throw new FHIRException("Problem evaluating slicing expression for element in profile " + profile.getUrl() + " path " + path + ": " + ex.getMessage()); } return ok; } // we assume that the following things are true: // the instance at root is valid against the schema and schematron // the instance validator had no issues against the base resource profile private void start(Object appContext, List<ValidationMessage> errors, Element resource, Element element, StructureDefinition defn, NodeStack stack) throws FHIRException, FHIRException, IOException { // profile is valid, and matches the resource name ResourceProfiles resourceProfiles = getResourceProfiles(element, stack); if (!resourceProfiles.isProcessed()) checkDeclaredProfiles(resourceProfiles, errors, resource, element, stack); if (!resourceProfiles.isProcessed()) { resourceProfiles.setProcessed(); if (!resourceProfiles.hasProfiles() && (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"))) { // Don't need to validate against the resource if there's a profile because the profile snapshot will include the relevant parts of the resources validateElement(appContext, errors, defn, defn.getSnapshot().getElement().get(0), null, null, resource, element, element.getName(), stack, false); } // specific known special validations if (element.getType().equals("Bundle")) validateBundle(errors, element, stack); if (element.getType().equals("Observation")) validateObservation(errors, element, stack); if (element.getType().equals("QuestionnaireResponse")) validateQuestionannaireResponse(errors, element, stack); } for (ProfileUsage profileUsage : resourceProfiles.uncheckedProfiles()) { profileUsage.setChecked(); validateElement(appContext, errors, profileUsage.getProfile(), profileUsage.getProfile().getSnapshot().getElement().get(0), null, null, resource, element, element.getName(), stack, false); } } private void validateQuestionannaireResponse(List<ValidationMessage> errors, Element element, NodeStack stack) { Element q = element.getNamedChild("questionnaire"); if (hint(errors, IssueType.REQUIRED, element.line(), element.col(), stack.getLiteralPath(), q != null && isNotBlank(q.getNamedChildValue("reference")), "No questionnaire is identified, so no validation can be performed against the base questionnaire")) { long t = System.nanoTime(); Questionnaire qsrc = context.fetchResource(Questionnaire.class, q.getNamedChildValue("reference")); sdTime = sdTime + (System.nanoTime() - t); if (warning(errors, IssueType.REQUIRED, q.line(), q.col(), stack.getLiteralPath(), qsrc != null, "The questionnaire could not be resolved, so no validation can be performed against the base questionnaire")) { boolean inProgress = "in-progress".equals(element.getNamedChildValue("status")); validateQuestionannaireResponseItems(qsrc, qsrc.getItem(), errors, element, stack, inProgress); } } } private void validateQuestionannaireResponseItem(Questionnaire qsrc, QuestionnaireItemComponent qItem, List<ValidationMessage> errors, Element element, NodeStack stack, boolean inProgress) { 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<Element> answers = new ArrayList<Element>(); element.getNamedChildren("answer", answers); if (inProgress) warning(errors, IssueType.REQUIRED, element.line(), element.col(), stack.getLiteralPath(), (answers.size() > 0) || !qItem.getRequired(), "No response answer found for required item "+qItem.getLinkId()); else rule(errors, IssueType.REQUIRED, element.line(), element.col(), stack.getLiteralPath(), (answers.size() > 0) || !qItem.getRequired(), "No response answer found for required item "+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); 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 (validateQuestionnaireResponseItemType(errors, answer, ns, "Quantity").equals("Quantity")) if (qItem.hasExtension("???")) validateQuestionnaireResponseItemQuantity(errors, answer, ns); break; case CHOICE: String itemType=validateQuestionnaireResponseItemType(errors, answer, ns, "Coding", "date", "time", "integer", "string"); 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.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; } validateQuestionannaireResponseItems(qsrc, qItem.getItem(), errors, answer, stack, inProgress); } 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.GROUP) { validateQuestionannaireResponseItems(qsrc, qItem.getItem(), errors, element, stack, inProgress); } else { List<Element> items = new ArrayList<Element>(); element.getNamedChildren("item", items); for (Element item : items) { NodeStack ns = stack.push(item, -1, null, null); rule(errors, IssueType.STRUCTURE, answers.get(0).line(), answers.get(0).col(), stack.getLiteralPath(), false, "Items not of type group should not have items - Item with linkId {0} of type {1} has {2} item(s)", qItem.getLinkId(), qItem.getType(), items.size()); } } } private void validateQuestionannaireResponseItem(Questionnaire qsrc, QuestionnaireItemComponent qItem, List<ValidationMessage> errors, List<Element> elements, NodeStack stack, boolean inProgress) { 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"); for (Element element : elements) { NodeStack ns = stack.push(element, -1, null, null); validateQuestionannaireResponseItem(qsrc, qItem, errors, element, ns, inProgress); } } private int getLinkIdIndex(List<QuestionnaireItemComponent> 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(Questionnaire qsrc, List<QuestionnaireItemComponent> qItems, List<ValidationMessage> errors, Element element, NodeStack stack, boolean inProgress) { List<Element> items = new ArrayList<Element>(); element.getNamedChildren("item", items); // now, sort into stacks Map<String, List<Element>> map = new HashMap<String, List<Element>>(); 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, "Structural Error: item is in the wrong place"); NodeStack ns = stack.push(item, -1, null, null); validateQuestionannaireResponseItem(qsrc, qItem, errors, element, ns, inProgress); } 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; List<Element> mapItem = map.get(linkId); if (mapItem == null) { mapItem = new ArrayList<Element>(); map.put(linkId, mapItem); } 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<Element> mapItem = map.get(qItem.getLinkId()); if (mapItem != null) validateQuestionannaireResponseItem(qsrc, qItem, errors, mapItem, stack, inProgress); else rule(errors, IssueType.REQUIRED, element.line(), element.col(), stack.getLiteralPath(), !qItem.getRequired(), "No response found for required item "+qItem.getLinkId()); } } private void validateQuestionnaireResponseItemQuantity( List<ValidationMessage> errors, Element answer, NodeStack stack) { } private String validateQuestionnaireResponseItemType(List<ValidationMessage> errors, Element element, NodeStack stack, String... types) { List<Element> values = new ArrayList<Element>(); element.getNamedChildrenWithWildcard("value[x]", values); 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]); 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<QuestionnaireItemComponent> 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<ValidationMessage> errors, Element value, NodeStack stack, List<Coding> optionList) { String system = value.getNamedChildValue("system"); String code = value.getNamedChildValue("code"); boolean found = false; for (Coding c : optionList) { if (ObjectUtil.equals(c.getSystem(), system) && ObjectUtil.equals(c.getCode(), code)) { found = true; break; } } rule(errors, IssueType.STRUCTURE, value.line(), value.col(), stack.getLiteralPath(), found, "The code "+system+"::"+code+" is not a valid option"); }*/ private void validateAnswerCode(List<ValidationMessage> errors, Element value, NodeStack stack, Questionnaire qSrc, Reference 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")) { try { Coding c = readAsCoding(value); if (isBlank(c.getCode()) && isBlank(c.getSystem()) && isNotBlank(c.getDisplay())) { if (theOpenChoice) { return; } } long t = System.nanoTime(); ValidationResult res = context.validateCode(c, vs); txTime = txTime + (System.nanoTime() - t); if (!res.isOk()) rule(errors, 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"); } catch (Exception e) { warning(errors, IssueType.CODEINVALID, value.line(), value.col(), stack.getLiteralPath(), false, "Error " + e.getMessage() + " validating Coding against Questionnaire Options"); } } } private void validateAnswerCode( List<ValidationMessage> 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.getOption().size() > 0) checkCodingOption(errors, answer, stack, qSrc, qItem, theOpenChoice); // validateAnswerCode(errors, v, stack, qItem.getOption()); else if (qItem.hasOptions()) validateAnswerCode(errors, v, stack, qSrc, qItem.getOptions(), theOpenChoice); else hint(errors, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), false, "Cannot validate options because no option or options are provided"); } private void checkOption( List<ValidationMessage> errors, Element answer, NodeStack stack, Questionnaire qSrc, QuestionnaireItemComponent qItem, String type) { checkOption(errors, answer, stack, qSrc, qItem, type, false); } private void checkOption( List<ValidationMessage> 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<ValidationMessage> 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.getOption().size() > 0) { List<IntegerType> list = new ArrayList<IntegerType>(); for (QuestionnaireItemOptionComponent components : qItem.getOption()) { 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 checkDateOption( List<ValidationMessage> 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.getOption().size() > 0) { List<DateType> list = new ArrayList<DateType>(); for (QuestionnaireItemOptionComponent components : qItem.getOption()) { 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"); } private void checkTimeOption( List<ValidationMessage> 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.getOption().size() > 0) { List<TimeType> list = new ArrayList<TimeType>(); for (QuestionnaireItemOptionComponent components : qItem.getOption()) { 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"); } private void checkStringOption( List<ValidationMessage> 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.getOption().size() > 0) { List<StringType> list = new ArrayList<StringType>(); for (QuestionnaireItemOptionComponent components : qItem.getOption()) { try { if (components.getValue() != null) { list.add(components.getValueStringType()); } } 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 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 checkCodingOption( List<ValidationMessage> 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.getOption().size() > 0) { List<Coding> list = new ArrayList<Coding>(); for (QuestionnaireItemOptionComponent components : qItem.getOption()) { 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 boolean typesAreAllReference(List<TypeRefComponent> theType) { for (TypeRefComponent typeRefComponent : theType) { if (typeRefComponent.getCode().equals("Reference") == false) { return false; } } return true; } private void validateBundle(List<ValidationMessage> errors, Element bundle, NodeStack stack) { List<Element> entries = new ArrayList<Element>(); bundle.getNamedChildren("entry", entries); String type = bundle.getNamedChildValue("type"); 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"); } else { Element firstEntry = entries.get(0); NodeStack firstStack = stack.push(firstEntry, 0, null, null); String fullUrl = firstEntry.getNamedChildValue("fullUrl"); if (type.equals("document")) { Element resource = firstEntry.getNamedChild("resource"); NodeStack localStack = firstStack.push(resource, -1, null, null); 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, localStack.push(resource, -1, null, null), fullUrl, id); } checkAllInterlinked(errors, entries, stack, bundle); } if (type.equals("message")) { Element resource = firstEntry.getNamedChild("resource"); NodeStack localStack = firstStack.push(resource, -1, null, null); 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, localStack.push(resource, -1, null, null), fullUrl, id); } checkAllInterlinked(errors, entries, stack, bundle); } } } private void checkAllInterlinked(List<ValidationMessage> errors, List<Element> entries, NodeStack stack, Element bundle) { List<Element> visitedResources = new ArrayList<Element>(); HashMap<Element,Element> candidateEntries = new HashMap<Element,Element>(); List<Element> candidateResources = new ArrayList<Element>(); for (Element entry: entries) { candidateEntries.put(entry.getNamedChild("resource"), entry); candidateResources.add(entry.getNamedChild("resource")); } followResourceLinks(entries.get(0), visitedResources, candidateEntries, candidateResources, true, errors, stack); List<Element> unusedResources = new ArrayList<Element>(); unusedResources.addAll(candidateResources); unusedResources.removeAll(visitedResources); int i = 0; for (Element entry : entries) { warning(errors, IssueType.INFORMATIONAL, entry.line(), entry.col(), stack.addToLiteralPath("entry", Integer.toString(i)), !unusedResources.contains(entry.getNamedChild("resource")), "Entry isn't reachable by traversing from first Bundle entry"); i++; } // Todo - check if the remaining resources point *to* the elements in the referenced set. Any that are still left over are errors } private void followResourceLinks(Element entry, List<Element> visitedResources, HashMap<Element, Element> candidateEntries, List<Element> candidateResources, boolean referenced, List<ValidationMessage> errors, NodeStack stack) { Element resource = entry.getNamedChild("resource"); if (visitedResources.contains(resource)) return; if (referenced) visitedResources.add(resource); List<String> references = findReferences(resource); for (String reference: references) { Element r = getFromBundle(stack.getElement(), reference, entry.getChildValue("fullUrl"), errors, stack.addToLiteralPath("entry:" + candidateResources.indexOf(resource))); if (r!=null && !visitedResources.contains(r)) { followResourceLinks(candidateEntries.get(r), visitedResources, candidateEntries, candidateResources, referenced, errors, stack); } } } private List<String> findReferences(Element start) { List<String> references = new ArrayList<String>(); findReferences(start, references); return references; } private void findReferences(Element start, List<String> references) { for (Element child : start.getChildren()) { if (child.getType().equals("Reference")) { String ref = child.getChildValue("reference"); if (ref!=null && !ref.startsWith("#")) references.add(ref); } findReferences(child, references); } } private void validateBundleReference(List<ValidationMessage> errors, List<Element> entries, Element ref, String name, NodeStack stack, String fullUrl, String type, String id) { String reference = null; try { reference = ref.getNamedChildValue("reference"); } catch (Error e) { } if (ref != null && !Utilities.noString(reference)) { Element target = resolveInBundle(entries, reference, fullUrl, type, id); rule(errors, IssueType.INVALID, ref.line(), ref.col(), stack.addToLiteralPath("reference"), target != null, "Unable to resolve the target of the reference in the bundle (" + name + ")"); } } private void validateContains(Object appContext, List<ValidationMessage> errors, String path, ElementDefinition child, ElementDefinition context, Element resource, Element element, NodeStack stack, IdStatus idstatus) throws FHIRException, FHIRException, IOException { String resourceName = element.getType(); 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 if (element.getSpecial() == SpecialElement.BUNDLE_ENTRY || element.getSpecial() == SpecialElement.BUNDLE_OUTCOME || element.getSpecial() == SpecialElement.PARAMETER ) resource = element; if (rule(errors, IssueType.INVALID, element.line(), element.col(), stack.getLiteralPath(), profile != null, "No profile found for contained resource of type '" + resourceName + "'")) validateResource(appContext, errors, resource, element, profile, null, idstatus, stack); } private void validateDocument(List<ValidationMessage> errors, List<Element> entries, Element composition, NodeStack stack, String fullUrl, String id) { // first entry must be a composition if (rule(errors, IssueType.INVALID, composition.line(), composition.col(), stack.getLiteralPath(), composition.getType().equals("Composition"), "The first entry in a document must be a composition")) { // the composition subject and section references must resolve in the bundle Element elem = composition.getNamedChild("subject"); if (rule(errors, IssueType.INVALID, composition.line(), composition.col(), stack.getLiteralPath(), elem != null, "A document composition must have a subject")) validateBundleReference(errors, entries, elem, "Composition Subject", stack.push(elem, -1, null, null), fullUrl, "Composition", id); validateSections(errors, entries, composition, stack, fullUrl, id); } } // rule(errors, IssueType.INVALID, bundle.line(), bundle.col(), "Bundle", !"urn:guid:".equals(base), "The base 'urn:guid:' is not valid (use urn:uuid:)"); // rule(errors, IssueType.INVALID, entry.line(), entry.col(), localStack.getLiteralPath(), !"urn:guid:".equals(ebase), "The base 'urn:guid:' is not valid"); // rule(errors, IssueType.INVALID, entry.line(), entry.col(), localStack.getLiteralPath(), !Utilities.noString(base) || !Utilities.noString(ebase), "entry // does not have a base"); // String firstBase = null; // firstBase = ebase == null ? base : ebase; private void validateElement(Object appContext, List<ValidationMessage> errors, StructureDefinition profile, ElementDefinition definition, StructureDefinition cprofile, ElementDefinition context, Element resource, Element element, String actualType, NodeStack stack, boolean inCodeableConcept) throws FHIRException, FHIRException, IOException { element.markValidation(profile, definition); // System.out.println(" "+stack.getLiteralPath()+" "+Long.toString((System.nanoTime() - time) / 1000000)); // time = System.nanoTime(); if (resource.getName().equals("contained")) { NodeStack ancestor = stack; while (!ancestor.element.isResource() || ancestor.element.getName().equals("contained")) ancestor = ancestor.parent; checkInvariants(errors, stack.getLiteralPath(), profile, definition, null, null, ancestor.element, element); } else checkInvariants(errors, stack.getLiteralPath(), profile, definition, null, null, resource, element); if (definition.getFixed()!=null) checkFixedValue(errors, stack.getLiteralPath(), element, definition.getFixed(), definition.getSliceName(), null); // get the list of direct defined children, including slices List<ElementDefinition> 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. StructureDefinition dt = this.context.fetchResource(StructureDefinition.class, "http://hl7.org/fhir/StructureDefinition/" + actualType); if (dt == null) throw new DefinitionException("Unable to resolve actual type " + actualType); childDefinitions = ProfileUtilities.getChildMap(dt, dt.getSnapshot().getElement().get(0)); } // 1. List the children, and remember their exact path (convenience) List<ElementInfo> children = new ArrayList<InstanceValidator.ElementInfo>(); ChildIterator iter = new ChildIterator(stack.getLiteralPath(), element); while (iter.next()) children.add(new ElementInfo(iter.name(), iter.element(), iter.path(), iter.count())); // 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<String> problematicPaths = new ArrayList<String>(); 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())) throw new DefinitionException("Slice encountered midway through path on " + slicer.getPath()); slicer = ed; process = false; sliceOffset = i; } else if (slicer != null && !slicer.getPath().equals(ed.getPath())) slicer = null; // if (process) { for (ElementInfo ei : children) { boolean match = false; if (slicer == null || slicer == ed) { match = nameMatches(ei.name, tail(ed.getPath())); } else { // ei.slice = slice; if (nameMatches(ei.name, tail(ed.getPath()))) try { match = sliceMatches(appContext, ei.element, ei.path, slicer, ed, profile, errors, stack); if (match) ei.slice = slicer; } catch (FHIRException e) { warning(errors, IssueType.PROCESSING, ei.line(), ei.col(), ei.path, false, e.getMessage()); unsupportedSlicing = true; childUnsupportedSlicing = true; } } if (match) { if (rule(errors, IssueType.INVALID, ei.line(), ei.col(), ei.path, ei.definition == null || ei.definition == slicer, "Profile " + profile.getUrl() + ", Element matches more than one slice")) { 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()); } } // } } int last = -1; int lastSlice = -1; for (ElementInfo ei : children) { String sliceInfo = ""; if (slicer != null) sliceInfo = " (slice: " + slicer.getPath()+")"; if (ei.path.endsWith(".extension")) rule(errors, IssueType.INVALID, ei.line(), ei.col(), ei.path, ei.definition != null, "Element is unknown or does not match any slice (url=\"" + ei.element.getNamedChildValue("url") + "\")" + (profile==null ? "" : " for profile " + profile.getUrl())); else if (!unsupportedSlicing) if (ei.slice!=null && (ei.slice.getSlicing().getRules().equals(ElementDefinition.SlicingRules.OPEN) || ei.slice.getSlicing().getRules().equals(ElementDefinition.SlicingRules.OPEN))) hint(errors, IssueType.INFORMATIONAL, ei.line(), ei.col(), ei.path, (ei.definition != null), "Element " + ei.element.getName() + " is unknown or does not match any slice " + sliceInfo + (profile==null ? "" : " for profile " + profile.getUrl())); else if (ei.slice!=null && (ei.slice.getSlicing().getRules().equals(ElementDefinition.SlicingRules.OPEN) || ei.slice.getSlicing().getRules().equals(ElementDefinition.SlicingRules.OPEN))) rule(errors, IssueType.INVALID, ei.line(), ei.col(), ei.path, (ei.definition != null), "Element " + ei.element.getName() + " is unknown or does not match any slice " + sliceInfo + (profile==null ? "" : " for profile " + profile.getUrl())); else hint(errors, IssueType.NOTSUPPORTED, ei.line(), ei.col(), ei.path, (ei.definition != null), "Could not verify slice for 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<PropertyRepresentation> r : ei.definition.getRepresentation()) { if (r.getValue() == PropertyRepresentation.XMLATTR) { isXmlAttr = true; break; } } rule(errors, IssueType.INVALID, ei.line(), ei.col(), ei.path, (ei.definition == null) || (ei.index >= last) || isXmlAttr, "As specified by profile " + profile.getUrl() + ", Element '"+ei.name+"' is out of order"); if (ei.slice != null && ei.index == last && ei.slice.getSlicing().getOrdered()) rule(errors, IssueType.INVALID, ei.line(), ei.col(), ei.path, (ei.definition == null) || (ei.sliceindex >= lastSlice) || isXmlAttr, "As specified by profile " + profile.getUrl() + ", Element '"+ei.name+"' 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; } // 3. report any definitions that have a cardinality problem for (ElementDefinition ed : childDefinitions) { if (ed.getRepresentation().isEmpty()) { // ignore xml attributes int count = 0; List<ElementDefinition> 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() + "]": ""); 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)); } } } // 4. check order if any slices are ordered. (todo) // 5. inspect each child for validity for (ElementInfo ei : children) { List<String> profiles = new ArrayList<String>(); if (ei.definition != null) { String type = null; ElementDefinition typeDefn = null; if (ei.definition.getType().size() == 1 && !ei.definition.getType().get(0).getCode().equals("*") && !ei.definition.getType().get(0).getCode().equals("Element") && !ei.definition.getType().get(0).getCode().equals("BackboneElement")) { type = ei.definition.getType().get(0).getCode(); // Excluding reference is a kludge to get around versioning issues if (ei.definition.getType().get(0).hasProfile() && !type.equals("Reference")) profiles.add(ei.definition.getType().get(0).getProfile()); } else if (ei.definition.getType().size() == 1 && ei.definition.getType().get(0).getCode().equals("*")) { String prefix = tail(ei.definition.getPath()); assert prefix.endsWith("[x]"); type = ei.name.substring(prefix.length() - 3); if (isPrimitiveType(type)) type = Utilities.uncapitalize(type); // Excluding reference is a kludge to get around versioning issues if (ei.definition.getType().get(0).hasProfile() && !type.equals("Reference")) profiles.add(ei.definition.getType().get(0).getProfile()); } else if (ei.definition.getType().size() > 1) { String prefix = tail(ei.definition.getPath()); assert typesAreAllReference(ei.definition.getType()) || prefix.endsWith("[x]") : prefix; prefix = prefix.substring(0, prefix.length() - 3); for (TypeRefComponent t : ei.definition.getType()) if ((prefix + Utilities.capitalize(t.getCode())).equals(ei.name)) { type = t.getCode(); // Excluding reference is a kludge to get around versioning issues if (t.hasProfile() && !type.equals("Reference")) profiles.add(t.getProfile()); } if (type == null) { TypeRefComponent trc = ei.definition.getType().get(0); if (trc.getCode().equals("Reference")) type = "Reference"; else rule(errors, IssueType.STRUCTURE, ei.line(), ei.col(), stack.getLiteralPath(), false, "The element " + ei.name + " 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()); } if (type != null) { if (type.startsWith("@")) { ei.definition = findElement(profile, type.substring(1)); type = null; } } NodeStack localStack = stack.push(ei.element, ei.count, ei.definition, type == null ? typeDefn : resolveType(type)); String localStackLiterapPath = localStack.getLiteralPath(); String eiPath = ei.path; assert(eiPath.equals(localStackLiterapPath)) : "ei.path: " + ei.path + " - localStack.getLiteralPath: " + localStackLiterapPath; boolean thisIsCodeableConcept = false; if (type != null) { if (isPrimitiveType(type)) { checkPrimitive(appContext, errors, ei.path, type, ei.definition, ei.element, profile); } else { if (type.equals("Identifier")) checkIdentifier(errors, ei.path, ei.element, ei.definition); else if (type.equals("Coding")) checkCoding(errors, ei.path, ei.element, profile, ei.definition, inCodeableConcept); else if (type.equals("CodeableConcept")) { checkCodeableConcept(errors, ei.path, ei.element, profile, ei.definition); thisIsCodeableConcept = true; } else if (type.equals("Reference")) checkReference(appContext, errors, ei.path, ei.element, 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 if (type.equals("Extension") && ei.element.getChildValue("url").contains("/")) checkExtension(appContext, errors, ei.path, resource, ei.element, ei.definition, profile, localStack); else if (type.equals("Resource")) validateContains(appContext, errors, ei.path, ei.definition, definition, resource, ei.element, localStack, idStatusForEntry(element, ei)); // if // (str.matches(".*([.,/])work\\1$")) else { StructureDefinition p = null; boolean elementValidated = false; if (profiles.isEmpty()) { p = getProfileForType(type); rule(errors, IssueType.STRUCTURE, ei.line(), ei.col(), ei.path, p != null, "Unknown type " + type); } else if (profiles.size()==1) { p = this.context.fetchResource(StructureDefinition.class, profiles.get(0)); rule(errors, IssueType.STRUCTURE, ei.line(), ei.col(), ei.path, p != null, "Unknown profile " + profiles.get(0)); } else { elementValidated = true; HashMap<String, List<ValidationMessage>> goodProfiles = new HashMap<String, List<ValidationMessage>>(); List<List<ValidationMessage>> badProfiles = new ArrayList<List<ValidationMessage>>(); for (String typeProfile : profiles) { p = this.context.fetchResource(StructureDefinition.class, typeProfile); if (rule(errors, IssueType.STRUCTURE, ei.line(), ei.col(), ei.path, p != null, "Unknown profile " + typeProfile)) { List<ValidationMessage> profileErrors = new ArrayList<ValidationMessage>(); validateElement(appContext, profileErrors, p, p.getSnapshot().getElement().get(0), profile, ei.definition, resource, ei.element, type, localStack, thisIsCodeableConcept); boolean hasError = false; for (ValidationMessage msg : profileErrors) { if (msg.getLevel()==ValidationMessage.IssueSeverity.ERROR || msg.getLevel()==ValidationMessage.IssueSeverity.FATAL) { hasError = true; break; } } if (hasError) badProfiles.add(profileErrors); else goodProfiles.put(typeProfile, profileErrors); } if (goodProfiles.size()==1) { errors.addAll(goodProfiles.get(0)); } else if (goodProfiles.size()==0) { rule(errors, IssueType.STRUCTURE, ei.line(), ei.col(), ei.path, false, "Unable to find matching profile among choices: " + StringUtils.join("; ", profiles)); for (List<ValidationMessage> messages : badProfiles) { errors.addAll(messages); } } else { warning(errors, IssueType.STRUCTURE, ei.line(), ei.col(), ei.path, false, "Found multiple matching profiles among choices: " + StringUtils.join("; ", goodProfiles.keySet())); for (List<ValidationMessage> messages : goodProfiles.values()) { errors.addAll(messages); } } } } if (p!=null) { if (!elementValidated) validateElement(appContext, errors, p, p.getSnapshot().getElement().get(0), profile, ei.definition, resource, ei.element, type, localStack, thisIsCodeableConcept); 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(appContext, errors, profile, ei.definition, null, null, resource, ei.element, type, localStack, thisIsCodeableConcept); } } } } } else { if (rule(errors, IssueType.STRUCTURE, ei.line(), ei.col(), stack.getLiteralPath(), ei.definition != null, "Unrecognised Content " + ei.name)) validateElement(appContext, errors, profile, ei.definition, null, null, resource, ei.element, type, localStack, false); } } } } private IdStatus idStatusForEntry(Element ep, ElementInfo ei) { if (isBundleEntry(ei.path)) { 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.path) || isBundleOutcome(ei.path)) return IdStatus.OPTIONAL; else return IdStatus.REQUIRED; } private void checkInvariants(List<ValidationMessage> errors, String path, StructureDefinition profile, ElementDefinition ed, String typename, String typeProfile, Element resource, Element element) throws FHIRException, FHIRException { if (noInvariantChecks) return; for (ElementDefinitionConstraintComponent inv : ed.getConstraint()) { if (inv.hasExpression()) { ExpressionNode n = (ExpressionNode) inv.getUserData("validator.expression.cache"); if (n == null) { long t = System.nanoTime(); try { n = fpe.parse(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(resource, element, n); fpeTime = fpeTime + (System.nanoTime() - t); msg = fpe.forLog(); } catch (Exception ex) { ok = false; msg = ex.getMessage(); } if (!ok) { try { ok = fpe.evaluateToBoolean(resource, element, n); } catch (PathEngineException e) { throw new FHIRException("Problem processing expression "+inv.getExpression() +" in profile " + profile.getUrl() + " path " + path + ": " + e.getMessage()); } if (!Utilities.noString(msg)) msg = " ("+msg+")"; if (inv.getSeverity() == ConstraintSeverity.ERROR) rule(errors, IssueType.INVARIANT, element.line(), element.col(), path, ok, inv.getHuman()+msg+" ["+inv.getExpression()+"]"); else if (inv.getSeverity() == ConstraintSeverity.WARNING) warning(errors, IssueType.INVARIANT, element.line(), element.line(), path, ok, inv.getHuman()+msg+" ["+inv.getExpression()+"]"); } } } } private void validateMessage(List<ValidationMessage> errors, List<Element> entries, Element messageHeader, NodeStack stack, String fullUrl, String id) { // first entry must be a messageheader if (rule(errors, IssueType.INVALID, messageHeader.line(), messageHeader.col(), stack.getLiteralPath(), messageHeader.getType().equals("MessageHeader"), "The first entry in a message must be a MessageHeader")) { // the composition subject and section references must resolve in the bundle List<Element> elements = messageHeader.getChildren("data"); for (Element elem: elements) validateBundleReference(errors, entries, elem, "MessageHeader Data", stack.push(elem, -1, null, null), fullUrl, "MessageHeader", id); } } private void validateObservation(List<ValidationMessage> errors, Element element, NodeStack stack) { // all observations should have a subject, a performer, and a time bpCheck(errors, IssueType.INVALID, element.line(), element.col(), stack.getLiteralPath(), element.getNamedChild("subject") != null, "All observations should have a subject"); bpCheck(errors, IssueType.INVALID, element.line(), element.col(), stack.getLiteralPath(), element.getNamedChild("performer") != null, "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 */ /* private void validateResource(List<ValidationMessage> errors, Element resource, Element element, StructureDefinition defn, ValidationProfileSet profiles, IdStatus idstatus, NodeStack stack) throws FHIRException, FHIRException { List<StructureDefinition> declProfiles = new ArrayList<StructureDefinition>(); List<Element> meta = element.getChildrenByName("meta"); if (!meta.isEmpty()) { for (Element profileName : meta.get(0).getChildrenByName("profile")) { StructureDefinition sd = context.fetchResource(StructureDefinition.class, profileName.getValue()); if (sd != null) declProfiles.add(sd); } } if (!declProfiles.isEmpty()) { // Validate against profiles rather than the resource itself as they'll be more constrained and will cover the resource elements anyhow for (StructureDefinition sd : declProfiles) validateResource2(errors, resource, element, sd, profiles, idstatus, stack); } else validateResource2(errors, resource, element, defn, profiles, idstatus, stack); }*/ private void validateResource(Object appContext, List<ValidationMessage> errors, Element resource, Element element, StructureDefinition defn, ValidationProfileSet profiles, IdStatus idstatus, NodeStack stack) throws FHIRException, FHIRException, IOException { 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); if (profiles!=null) getResourceProfiles(resource, stack).addProfiles(errors, profiles, stack.getLiteralPath(), element); 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")) { Element first = getFirstEntry(element); if (first != null && first.getType().equals(type)) { element = first; resourceName = element.getType(); idstatus = IdStatus.OPTIONAL; // why? } } 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(appContext, errors, resource, element, defn, stack); // root is both definition and type } } private void loadProfiles(ValidationProfileSet profiles) throws DefinitionException { if (profiles != null) { for (String profile : profiles.getCanonicalUrls()) { StructureDefinition p = context.fetchResource(StructureDefinition.class, profile); if (p == null) throw new DefinitionException("StructureDefinition '" + profile + "' not found"); profiles.getDefinitions().add(p); } } } private Element getFirstEntry(Element bundle) { List<Element> list = new ArrayList<Element>(); bundle.getNamedChildren("entry", list); if (list.isEmpty()) return null; Element resource = list.get(0).getNamedChild("resource"); if (resource == null) return null; else return resource; } private void validateSections(List<ValidationMessage> errors, List<Element> entries, Element focus, NodeStack stack, String fullUrl, String id) { List<Element> sections = new ArrayList<Element>(); focus.getNamedChildren("entry", sections); int i = 0; for (Element section : sections) { NodeStack localStack = stack.push(section, 1, null, null); validateBundleReference(errors, entries, section.getNamedChild("content"), "Section Content", localStack, fullUrl, "Composition", id); validateSections(errors, entries, section, localStack, fullUrl, id); i++; } } private boolean valueMatchesCriteria(Element value, ElementDefinition criteria) throws FHIRException { if (criteria.hasFixed()) { List<ValidationMessage> msgs = new ArrayList<ValidationMessage>(); checkFixedValue(msgs, "{virtual}", value, criteria.getFixed(), "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 <= 2100; } catch (NumberFormatException e) { return false; } } public class ChildIterator { private String basePath; private Element parent; private int cursor; private int lastCount; public ChildIterator(String path, Element element) { parent = element; basePath = path; cursor = -1; } public int count() { String nb = cursor == 0 ? "--" : parent.getChildren().get(cursor-1).getName(); String na = cursor >= parent.getChildren().size() - 1 ? "--" : parent.getChildren().get(cursor+1).getName(); if (name().equals(nb) || name().equals(na) ) { return lastCount + 1; } else return -1; } public Element element() { return parent.getChildren().get(cursor); } public String name() { return element().getName(); } public boolean next() { if (cursor == -1) { cursor++; lastCount = 0; } else { String lastName = name(); cursor++; if (cursor < parent.getChildren().size() && name().equals(lastName)) lastCount++; else lastCount = 0; } return cursor < parent.getChildren().size(); } public String path() { int i = count(); String sfx = ""; if (i > -1) { sfx = "[" + Integer.toString(lastCount + 1) + "]"; } return basePath + "." + name() + sfx; } } public class NodeStack { private ElementDefinition definition; private Element element; private ElementDefinition extension; private String literalPath; // xpath format private List<String> logicalPaths; // dotted format, various entry points private NodeStack parent; private ElementDefinition type; public NodeStack() { } public NodeStack(Element element) { this.element = element; literalPath = element.getName(); } 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<String> getLogicalPaths() { return logicalPaths == null ? new ArrayList<String>() : logicalPaths; } private ElementDefinition getType() { return type; } private NodeStack push(Element element, int count, ElementDefinition definition, ElementDefinition type) { NodeStack res = new NodeStack(); res.parent = this; res.element = element; res.definition = definition; res.literalPath = getLiteralPath() + "." + element.getName(); if (count > -1) res.literalPath = res.literalPath + "[" + Integer.toString(count) + "]"; res.logicalPaths = new ArrayList<String>(); if (type != null) { // type will be bull if we on a stitching point of a contained resource, or if.... res.type = type; String t = tail(definition.getPath()); 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(type.getPath()); } else if (definition != null) { for (String lp : getLogicalPaths()) res.logicalPaths.add(lp + "." + element.getName()); } else res.logicalPaths.addAll(getLogicalPaths()); // CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder(); // for (String lp : res.logicalPaths) // b.append(lp); // System.out.println(res.literalPath+" : "+b.toString()); return res; } private void setType(ElementDefinition type) { this.type = type; } } private void checkForProcessingInstruction(List<ValidationMessage> errors, Document document) { Node node = document.getFirstChild(); while (node != null) { rule(errors, IssueType.INVALID, -1, -1, "(document)", node.getNodeType() != Node.PROCESSING_INSTRUCTION_NODE, "No processing instructions allowed in resources"); node = node.getNextSibling(); } } public class ElementInfo { public int index; // order of definition in overall order. all slices get the index of the slicing definition public int sliceindex; // order of the definition in the slices (if slice != null) public int count; public ElementDefinition definition; public ElementDefinition slice; private Element element; private String name; private String path; public ElementInfo(String name, Element element, String path, int count) { this.name = name; this.element = element; this.path = path; this.count = count; } public int col() { return element.col(); } public int line() { return element.line(); } } public String reportTimes() { String s = String.format("Times: overall = %d, tx = %d, sd = %d, load = %d, fpe = %d", overall, txTime, sdTime, loadTime, fpeTime); 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(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()); } } } } } } } }