package org.hl7.fhir.instance.validation; import java.util.ArrayList; import java.util.List; import java.util.Map.Entry; import org.hl7.fhir.instance.formats.FormatUtilities; import org.hl7.fhir.instance.model.Address; import org.hl7.fhir.instance.model.Attachment; import org.hl7.fhir.instance.model.CodeableConcept; import org.hl7.fhir.instance.model.Coding; import org.hl7.fhir.instance.model.ContactPoint; import org.hl7.fhir.instance.model.ElementDefinition; import org.hl7.fhir.instance.model.ElementDefinition.ElementDefinitionBindingComponent; import org.hl7.fhir.instance.model.ElementDefinition.TypeRefComponent; import org.hl7.fhir.instance.model.Enumerations.BindingStrength; import org.hl7.fhir.instance.model.Extension; import org.hl7.fhir.instance.model.HumanName; import org.hl7.fhir.instance.model.Identifier; import org.hl7.fhir.instance.model.OperationOutcome.IssueSeverity; import org.hl7.fhir.instance.model.OperationOutcome.IssueType; import org.hl7.fhir.instance.model.Period; import org.hl7.fhir.instance.model.Quantity; import org.hl7.fhir.instance.model.Range; import org.hl7.fhir.instance.model.Ratio; import org.hl7.fhir.instance.model.Reference; import org.hl7.fhir.instance.model.Resource; import org.hl7.fhir.instance.model.SampledData; import org.hl7.fhir.instance.model.StringType; import org.hl7.fhir.instance.model.StructureDefinition; import org.hl7.fhir.instance.model.StructureDefinition.ExtensionContext; import org.hl7.fhir.instance.model.StructureDefinition.StructureDefinitionKind; import org.hl7.fhir.instance.model.StructureDefinition.StructureDefinitionSnapshotComponent; import org.hl7.fhir.instance.model.Timing; import org.hl7.fhir.instance.model.Type; import org.hl7.fhir.instance.model.UriType; import org.hl7.fhir.instance.model.ValueSet; import org.hl7.fhir.instance.model.ValueSet.ConceptDefinitionComponent; import org.hl7.fhir.instance.model.ValueSet.ValueSetExpansionContainsComponent; import org.hl7.fhir.instance.terminologies.ValueSetExpander.ETooCostly; import org.hl7.fhir.instance.terminologies.ValueSetExpander.ValueSetExpansionOutcome; import org.hl7.fhir.instance.terminologies.ValueSetExpanderFactory; import org.hl7.fhir.instance.terminologies.ValueSetExpansionCache; import org.hl7.fhir.instance.utilities.CommaSeparatedStringBuilder; import org.hl7.fhir.instance.utilities.Utilities; import org.hl7.fhir.instance.utilities.xml.XMLUtil; import org.hl7.fhir.instance.utils.EOperationOutcome; import org.hl7.fhir.instance.utils.IWorkerContext; import org.hl7.fhir.instance.utils.IWorkerContext.ValidationResult; import org.hl7.fhir.instance.utils.ProfileUtilities; import org.hl7.fhir.instance.validation.ValidationMessage.Source; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonNull; import com.google.gson.JsonObject; import com.google.gson.JsonPrimitive; /* * todo: * check urn's don't start oid: or uuid: */ public class InstanceValidator extends BaseValidator implements IResourceValidator { private boolean anyExtensionsAllowed; private BestPracticeWarningLevel bpWarnings; private ValueSetExpanderFactory cache; // configuration items private CheckDisplayOption checkDisplay; private IWorkerContext context; private List<String> extensionDomains = new ArrayList<String>(); private boolean requiresResourceId; // used during the build process to keep the overall volume of messages down private boolean suppressLoincSnomedMessages; private boolean shouldCheckForIdPresence = true; public InstanceValidator(IWorkerContext theContext) throws Exception { super(); this.context = theContext; source = Source.InstanceValidator; cache = new ValueSetExpansionCache(theContext, null); } public InstanceValidator(IWorkerContext theContext, ValueSetExpanderFactory theValueSetExpander) throws Exception { super(); this.context = theContext; source = Source.InstanceValidator; this.cache = theValueSetExpander; } private boolean allowUnknownExtension(String url) { if (url.contains("example.org") || url.contains("acme.com") || url.contains("nema.org")) return true; for (String s : extensionDomains) if (url.startsWith(s)) return true; return anyExtensionsAllowed; } 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 } } } public void setShouldCheckForIdPresence(boolean theShouldCheckForIdPresence) { shouldCheckForIdPresence = theShouldCheckForIdPresence; } private boolean check(String v1, String v2) { return v1 == null ? Utilities.noString(v1) : v1.equals(v2); } private void checkAddress(List<ValidationMessage> errors, String path, WrapperElement focus, Address fixed) { checkFixedValue(errors, path + ".use", focus.getNamedChild("use"), fixed.getUseElement(), "use"); checkFixedValue(errors, path + ".text", focus.getNamedChild("text"), fixed.getTextElement(), "text"); checkFixedValue(errors, path + ".city", focus.getNamedChild("city"), fixed.getCityElement(), "city"); checkFixedValue(errors, path + ".state", focus.getNamedChild("state"), fixed.getStateElement(), "state"); checkFixedValue(errors, path + ".country", focus.getNamedChild("country"), fixed.getCountryElement(), "country"); checkFixedValue(errors, path + ".zip", focus.getNamedChild("zip"), fixed.getPostalCodeElement(), "postalCode"); List<WrapperElement> lines = new ArrayList<WrapperElement>(); 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"); } } private void checkAttachment(List<ValidationMessage> errors, String path, WrapperElement focus, Attachment fixed) { checkFixedValue(errors, path + ".contentType", focus.getNamedChild("contentType"), fixed.getContentTypeElement(), "contentType"); checkFixedValue(errors, path + ".language", focus.getNamedChild("language"), fixed.getLanguageElement(), "language"); checkFixedValue(errors, path + ".data", focus.getNamedChild("data"), fixed.getDataElement(), "data"); checkFixedValue(errors, path + ".url", focus.getNamedChild("url"), fixed.getUrlElement(), "url"); checkFixedValue(errors, path + ".size", focus.getNamedChild("size"), fixed.getSizeElement(), "size"); checkFixedValue(errors, path + ".hash", focus.getNamedChild("hash"), fixed.getHashElement(), "hash"); checkFixedValue(errors, path + ".title", focus.getNamedChild("title"), fixed.getTitleElement(), "title"); } // public API private boolean checkCode(List<ValidationMessage> errors, WrapperElement element, String path, String code, String system, String display) throws Exception { if (context.supportsSystem(system)) { ValidationResult s = context.validateCode(system, code, display); 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 { ValueSet vs = getValueSet(system); if (warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, vs != null, "Unknown Code System " + system)) { ConceptDefinitionComponent def = getCodeDefinition(vs, 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, WrapperElement focus, CodeableConcept fixed) { checkFixedValue(errors, path + ".text", focus.getNamedChild("text"), fixed.getTextElement(), "text"); List<WrapperElement> codings = new ArrayList<WrapperElement>(); 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"); } } private void checkCodeableConcept(List<ValidationMessage> errors, String path, WrapperElement element, StructureDefinition profile, ElementDefinition theElementCntext) throws EOperationOutcome, Exception { 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 (cc)")) { if (binding.hasValueSet() && binding.getValueSet() instanceof Reference) { ValueSet unexpandedVs = resolveBindingReference(binding.getValueSet()); if (warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, unexpandedVs != null, "ValueSet " + describeReference(binding.getValueSet()) + " not found")) { ValueSet vs; try { boolean found = false; boolean any = false; WrapperElement c = element.getFirstChild(); while (c != null) { if (c.getName().equals("coding")) { any = true; String system = c.getNamedChildValue("system"); String code = c.getNamedChildValue("code"); if (system != null && code != null) { ValueSetExpansionOutcome exp = cache.getExpander().expand(unexpandedVs); vs = exp != null ? exp.getValueset() : null; if (vs == null) { if (binding.getStrength() != BindingStrength.REQUIRED) { ValidationResult validationResult = context.validateCode(system, code, null); if (validationResult.isOk()) { found = true; } else { warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "Unable to validate code \"{0}\" in code system \"{1}\"", code, system); return; } } } if (found == false) { if (!warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, vs != null, "Unable to expand value set for " + describeReference(binding.getValueSet()))) { return; } } found = found || codeInExpansion(vs, system, code); } } c = c.getNextSibling(); } if (!any && binding.getStrength() == BindingStrength.REQUIRED) warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, found, "No code provided, and value set " + describeReference(binding.getValueSet()) + " (" + unexpandedVs.getUrl() + ") is required"); if (any) if (binding.getStrength() == BindingStrength.PREFERRED) hint(errors, IssueType.CODEINVALID, element.line(), element.col(), path, found, "None of the codes are in the example value set " + describeReference(binding.getValueSet()) + " (" + unexpandedVs.getUrl() + ")"); else if (binding.getStrength() == BindingStrength.EXTENSIBLE) warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, found, "None of the codes are in the expected value set " + describeReference(binding.getValueSet()) + " (" + unexpandedVs.getUrl() + ")"); } catch (Exception e) { if (e.getMessage() == null) { warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "Exception opening value set " + unexpandedVs.getUrl() + " for " + describeReference(binding.getValueSet()) + ": --Null--"); // } else if (!e.getMessage().contains("unable to find value set http://snomed.info/sct")) { // hint(errors, IssueType.CODEINVALID, path, suppressLoincSnomedMessages, "Snomed value set - not validated"); // } else if (!e.getMessage().contains("unable to find value set http://loinc.org")) { // hint(errors, IssueType.CODEINVALID, path, suppressLoincSnomedMessages, "Loinc value set - not validated"); } else warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "Exception opening value set " + unexpandedVs.getUrl() + " for " + describeReference(binding.getValueSet()) + ": " + e.getMessage()); } } } else if (binding.hasValueSet()) { hint(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "Binding by URI reference cannot be checked"); } else { hint(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "Binding for path " + path + " has no source, so can't be checked"); } } } } private void checkCoding(List<ValidationMessage> errors, String path, WrapperElement focus, Coding fixed) { checkFixedValue(errors, path + ".system", focus.getNamedChild("system"), fixed.getSystemElement(), "system"); checkFixedValue(errors, path + ".code", focus.getNamedChild("code"), fixed.getCodeElement(), "code"); checkFixedValue(errors, path + ".display", focus.getNamedChild("display"), fixed.getDisplayElement(), "display"); checkFixedValue(errors, path + ".userSelected", focus.getNamedChild("userSelected"), fixed.getUserSelectedElement(), "userSelected"); } private void checkCoding(List<ValidationMessage> errors, String path, WrapperElement element, StructureDefinition profile, ElementDefinition context) throws EOperationOutcome, Exception { 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) { if (checkCode(errors, element, path, code, system, display)) if (context != null && context.getBinding() != null) { ElementDefinitionBindingComponent binding = context.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 vs = resolveBindingReference(binding.getValueSet()); if (warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, vs != null, "ValueSet " + describeReference(binding.getValueSet()) + " not found")) { try { vs = cache.getExpander().expand(vs).getValueset(); if (warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, vs != null, "Unable to expand value set for " + describeReference(binding.getValueSet()))) { if (binding.getStrength() == BindingStrength.REQUIRED) rule(errors, IssueType.CODEINVALID, element.line(), element.col(), path, codeInExpansion(vs, system, code), "Code {" + system + "}" + code + " is not in value set " + describeReference(binding.getValueSet()) + " (" + vs.getUrl() + ")"); else if (binding.getStrength() == BindingStrength.EXTENSIBLE) warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, codeInExpansion(vs, system, code), "Code {" + system + "}" + code + " is not in value set " + describeReference(binding.getValueSet()) + " (" + vs.getUrl() + ")"); else hint(errors, IssueType.CODEINVALID, element.line(), element.col(), path, codeInExpansion(vs, system, code), "Code {" + system + "}" + code + " is not in value set " + describeReference(binding.getValueSet()) + " (" + vs.getUrl() + ")"); } } catch (Exception e) { if (e.getMessage() == null) warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "Exception opening value set " + vs.getUrl() + " for " + describeReference(binding.getValueSet()) + ": --Null--"); else warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "Exception opening value set " + vs.getUrl() + " for " + describeReference(binding.getValueSet()) + ": " + e.getMessage()); } } } else if (binding.hasValueSet()) { hint(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "Binding by URI reference cannot be checked"); } else { hint(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "Binding for path " + path + " has no source, so can't be checked"); } } } } } private void checkContactPoint(List<ValidationMessage> errors, String path, WrapperElement focus, ContactPoint fixed) { checkFixedValue(errors, path + ".system", focus.getNamedChild("system"), fixed.getSystemElement(), "system"); checkFixedValue(errors, path + ".value", focus.getNamedChild("value"), fixed.getValueElement(), "value"); checkFixedValue(errors, path + ".use", focus.getNamedChild("use"), fixed.getUseElement(), "use"); checkFixedValue(errors, path + ".period", focus.getNamedChild("period"), fixed.getPeriod(), "period"); } private void checkDeclaredProfiles(List<ValidationMessage> errors, WrapperElement element, NodeStack stack) throws Exception { WrapperElement meta = element.getNamedChild("meta"); if (meta != null) { List<WrapperElement> profiles = new ArrayList<InstanceValidator.WrapperElement>(); meta.getNamedChildren("profile", profiles); int i = 0; for (WrapperElement profile : profiles) { String ref = profile.getAttribute("value"); 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")) { StructureDefinition pr = context.fetchResource(StructureDefinition.class, ref); if (warning(errors, IssueType.INVALID, element.line(), element.col(), p, pr != null, "StructureDefinition reference \"{0}\" could not be resolved", ref)) { if (rule(errors, IssueType.STRUCTURE, element.line(), element.col(), p, pr.hasSnapshot(), "StructureDefinition has no snapshot - validation is against the snapshot, so it must be provided")) { validateElement(errors, pr, pr.getSnapshot().getElement().get(0), null, null, element, element.getName(), stack); } } i++; } } } } private StructureDefinition checkExtension(List<ValidationMessage> errors, String path, WrapperElement element, ElementDefinition def, StructureDefinition profile, NodeStack stack) throws Exception { String url = element.getAttribute("url"); boolean isModifier = element.getName().equals("modifierExtension"); StructureDefinition ex = context.fetchResource(StructureDefinition.class, url); if (ex == null) { if (!rule(errors, IssueType.STRUCTURE, element.line(), element.col(), path, allowUnknownExtension(url), "The extension " + url + " is unknown, and not allowed here")) warning(errors, IssueType.STRUCTURE, element.line(), element.col(), path, allowUnknownExtension(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)"); // 2. is the content of the extension valid? } return ex; } private boolean checkExtensionContext(List<ValidationMessage> errors, WrapperElement 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.MAPPING) { throw new Error("Not handled yet (extensionContext)"); } 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, WrapperElement focus, org.hl7.fhir.instance.model.Element fixed, String propName) { if (fixed == null && 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 && focus == null) rule(errors, IssueType.VALUE, 0, 0, path, false, "Mising element " + propName); else { String value = focus.getAttribute("value"); if (fixed instanceof org.hl7.fhir.instance.model.BooleanType) rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.instance.model.BooleanType) fixed).asStringValue(), value), "Value is '" + value + "' but must be '" + ((org.hl7.fhir.instance.model.BooleanType) fixed).asStringValue() + "'"); else if (fixed instanceof org.hl7.fhir.instance.model.IntegerType) rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.instance.model.IntegerType) fixed).asStringValue(), value), "Value is '" + value + "' but must be '" + ((org.hl7.fhir.instance.model.IntegerType) fixed).asStringValue() + "'"); else if (fixed instanceof org.hl7.fhir.instance.model.DecimalType) rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.instance.model.DecimalType) fixed).asStringValue(), value), "Value is '" + value + "' but must be '" + ((org.hl7.fhir.instance.model.DecimalType) fixed).asStringValue() + "'"); else if (fixed instanceof org.hl7.fhir.instance.model.Base64BinaryType) rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.instance.model.Base64BinaryType) fixed).asStringValue(), value), "Value is '" + value + "' but must be '" + ((org.hl7.fhir.instance.model.Base64BinaryType) fixed).asStringValue() + "'"); else if (fixed instanceof org.hl7.fhir.instance.model.InstantType) rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.instance.model.InstantType) fixed).getValue().toString(), value), "Value is '" + value + "' but must be '" + ((org.hl7.fhir.instance.model.InstantType) fixed).asStringValue() + "'"); else if (fixed instanceof org.hl7.fhir.instance.model.StringType) rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.instance.model.StringType) fixed).getValue(), value), "Value is '" + value + "' but must be '" + ((org.hl7.fhir.instance.model.StringType) fixed).getValue() + "'"); else if (fixed instanceof org.hl7.fhir.instance.model.UriType) rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.instance.model.UriType) fixed).getValue(), value), "Value is '" + value + "' but must be '" + ((org.hl7.fhir.instance.model.UriType) fixed).getValue() + "'"); else if (fixed instanceof org.hl7.fhir.instance.model.DateType) rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.instance.model.DateType) fixed).getValue().toString(), value), "Value is '" + value + "' but must be '" + ((org.hl7.fhir.instance.model.DateType) fixed).getValue() + "'"); else if (fixed instanceof org.hl7.fhir.instance.model.DateTimeType) rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.instance.model.DateTimeType) fixed).getValue().toString(), value), "Value is '" + value + "' but must be '" + ((org.hl7.fhir.instance.model.DateTimeType) fixed).getValue() + "'"); else if (fixed instanceof org.hl7.fhir.instance.model.OidType) rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.instance.model.OidType) fixed).getValue(), value), "Value is '" + value + "' but must be '" + ((org.hl7.fhir.instance.model.OidType) fixed).getValue() + "'"); else if (fixed instanceof org.hl7.fhir.instance.model.UuidType) rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.instance.model.UuidType) fixed).getValue(), value), "Value is '" + value + "' but must be '" + ((org.hl7.fhir.instance.model.UuidType) fixed).getValue() + "'"); else if (fixed instanceof org.hl7.fhir.instance.model.CodeType) rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.instance.model.CodeType) fixed).getValue(), value), "Value is '" + value + "' but must be '" + ((org.hl7.fhir.instance.model.CodeType) fixed).getValue() + "'"); else if (fixed instanceof org.hl7.fhir.instance.model.IdType) rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.instance.model.IdType) fixed).getValue(), value), "Value is '" + value + "' but must be '" + ((org.hl7.fhir.instance.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<WrapperElement> extensions = new ArrayList<WrapperElement>(); 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()) { WrapperElement 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.getFirstChild().getNextSibling(), e.getValue(), "extension.value"); } } } } } 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(); } } private void checkHumanName(List<ValidationMessage> errors, String path, WrapperElement focus, HumanName fixed) { checkFixedValue(errors, path + ".use", focus.getNamedChild("use"), fixed.getUseElement(), "use"); checkFixedValue(errors, path + ".text", focus.getNamedChild("text"), fixed.getTextElement(), "text"); checkFixedValue(errors, path + ".period", focus.getNamedChild("period"), fixed.getPeriod(), "period"); List<WrapperElement> parts = new ArrayList<WrapperElement>(); focus.getNamedChildren("family", parts); if (rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, parts.size() == fixed.getFamily().size(), "Expected " + Integer.toString(fixed.getFamily().size()) + " but found " + Integer.toString(parts.size()) + " family elements")) { for (int i = 0; i < parts.size(); i++) checkFixedValue(errors, path + ".family", parts.get(i), fixed.getFamily().get(i), "family"); } 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.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.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"); } } private void checkIdentifier(List<ValidationMessage> errors, String path, WrapperElement 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, WrapperElement focus, Identifier fixed) { checkFixedValue(errors, path + ".use", focus.getNamedChild("use"), fixed.getUseElement(), "use"); checkFixedValue(errors, path + ".type", focus.getNamedChild("type"), fixed.getType(), "type"); checkFixedValue(errors, path + ".system", focus.getNamedChild("system"), fixed.getSystemElement(), "system"); checkFixedValue(errors, path + ".value", focus.getNamedChild("value"), fixed.getValueElement(), "value"); checkFixedValue(errors, path + ".period", focus.getNamedChild("period"), fixed.getPeriod(), "period"); checkFixedValue(errors, path + ".assigner", focus.getNamedChild("assigner"), fixed.getAssigner(), "assigner"); } private void checkPeriod(List<ValidationMessage> errors, String path, WrapperElement focus, Period fixed) { checkFixedValue(errors, path + ".start", focus.getNamedChild("start"), fixed.getStartElement(), "start"); checkFixedValue(errors, path + ".end", focus.getNamedChild("end"), fixed.getEndElement(), "end"); } private void checkPrimitive(List<ValidationMessage> errors, String path, String type, ElementDefinition context, WrapperElement e) throws Exception { if (type.equals("uri")) { rule(errors, IssueType.INVALID, e.line(), e.col(), path, !e.getAttribute("value").startsWith("oid:"), "URI values cannot start with oid:"); rule(errors, IssueType.INVALID, e.line(), e.col(), path, !e.getAttribute("value").startsWith("uuid:"), "URI values cannot start with uuid:"); rule(errors, IssueType.INVALID, e.line(), e.col(), path, e.getAttribute("value").equals(e.getAttribute("value").trim()), "URI values cannot have leading or trailing whitespace"); } if (!type.equalsIgnoreCase("string") && e.hasAttribute("value")) { if (rule(errors, IssueType.INVALID, e.line(), e.col(), path, e.getAttribute("value").length() > 0, "@value cannot be empty")) { warning(errors, IssueType.INVALID, e.line(), e.col(), path, e.getAttribute("value").trim().equals(e.getAttribute("value")), "value should not start or finish with whitespace"); } } if (type.equals("dateTime")) { rule(errors, IssueType.INVALID, e.line(), e.col(), path, yearIsValid(e.getAttribute("value")), "The value '" + e.getAttribute("value") + "' does not have a valid year"); rule(errors, IssueType.INVALID, e.line(), e.col(), path, e.getAttribute("value") .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.getAttribute("value")) || hasTimeZone(e.getAttribute("value")), "if a date has a time, it must have a timezone"); } if (type.equals("instant")) { rule(errors, IssueType.INVALID, e.line(), e.col(), path, e.getAttribute("value").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.getAttribute("value") + "' is not valid (by regex)"); rule(errors, IssueType.INVALID, e.line(), e.col(), path, yearIsValid(e.getAttribute("value")), "The value '" + e.getAttribute("value") + "' does not have a valid year"); } if (type.equals("code")) { // 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.getAttribute("value")), "The code '" + e.getAttribute("value") + "' is not valid (whitespace rules)"); } if (context.hasBinding()) { checkPrimitiveBinding(errors, path, type, context, e); } // for nothing to check } // note that we don't check the type here; it could be string, uri or code. private void checkPrimitiveBinding(List<ValidationMessage> errors, String path, String type, ElementDefinition context, WrapperElement element) throws Exception { if (!element.hasAttribute("value")) return; String value = element.getAttribute("value"); // System.out.println("check "+value+" in "+path); // firstly, resolve the value set ElementDefinitionBindingComponent binding = context.getBinding(); if (binding.hasValueSet() && binding.getValueSet() instanceof Reference) { ValueSet vs = resolveBindingReference(binding.getValueSet()); if (warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, vs != null, "ValueSet {0} not found", describeReference(binding.getValueSet()))) { try { ValueSetExpansionOutcome expansionOutcome = cache.getExpander().expand(vs); vs = expansionOutcome != null ? expansionOutcome.getValueset() : null; if (warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, vs != null, "Unable to expand value set for {0}", describeReference(binding.getValueSet()))) { boolean ok = codeInExpansion(vs, null, value); if (binding.getStrength() == BindingStrength.REQUIRED) rule(errors, IssueType.CODEINVALID, element.line(), element.col(), path, ok, "Coded value {0} is not in value set {1} ({2})", value, describeReference(binding.getValueSet()), vs.getUrl()); else if (binding.getStrength() == BindingStrength.EXTENSIBLE) warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, ok, "Coded value {0} is not in value set {1} ({2})", value, describeReference(binding.getValueSet()), vs.getUrl()); else hint(errors, IssueType.CODEINVALID, element.line(), element.col(), path, ok, "Coded value {0} is not in value set {1} ({2})", value, describeReference(binding.getValueSet()), vs.getUrl()); } } catch (ETooCostly e) { if (e.getMessage() == null) warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "Exception opening value set " + vs.getUrl() + " for " + describeReference(binding.getValueSet()) + ": --Null--"); else warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "Exception opening value set " + vs.getUrl() + " for " + describeReference(binding.getValueSet()) + ": " + e.getMessage()); } } } else hint(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "Binding has no source, so can't be checked"); } private void checkQuantity(List<ValidationMessage> errors, String path, WrapperElement focus, Quantity fixed) { checkFixedValue(errors, path + ".value", focus.getNamedChild("value"), fixed.getValueElement(), "value"); checkFixedValue(errors, path + ".comparator", focus.getNamedChild("comparator"), fixed.getComparatorElement(), "comparator"); checkFixedValue(errors, path + ".units", focus.getNamedChild("unit"), fixed.getUnitElement(), "units"); checkFixedValue(errors, path + ".system", focus.getNamedChild("system"), fixed.getSystemElement(), "system"); checkFixedValue(errors, path + ".code", focus.getNamedChild("code"), fixed.getCodeElement(), "code"); } // implementation private void checkRange(List<ValidationMessage> errors, String path, WrapperElement focus, Range fixed) { checkFixedValue(errors, path + ".low", focus.getNamedChild("low"), fixed.getLow(), "low"); checkFixedValue(errors, path + ".high", focus.getNamedChild("high"), fixed.getHigh(), "high"); } private void checkRatio(List<ValidationMessage> errors, String path, WrapperElement focus, Ratio fixed) { checkFixedValue(errors, path + ".numerator", focus.getNamedChild("numerator"), fixed.getNumerator(), "numerator"); checkFixedValue(errors, path + ".denominator", focus.getNamedChild("denominator"), fixed.getDenominator(), "denominator"); } private void checkReference(List<ValidationMessage> errors, String path, WrapperElement element, StructureDefinition profile, ElementDefinition container, String parentType, NodeStack stack) throws Exception { String ref = element.getNamedChildValue("reference"); if (Utilities.noString(ref)) { // todo - what should we do in this case? hint(errors, IssueType.STRUCTURE, element.line(), element.col(), path, !Utilities.noString(element.getNamedChildValue("display")), "A Reference without an actual reference should have a display"); return; } WrapperElement we = resolve(ref, stack); String ft; if (we != null) ft = we.getResourceType(); else ft = tryParse(ref); if (hint(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.hasProfile() || type.getProfile().get(0).getValue().equals("http://hl7.org/fhir/StructureDefinition/Resource")) ok = true; else { String pr = type.getProfile().get(0).getValue(); String bt = getBaseType(profile, pr); 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); } else ok = true; // suppress following check } } 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() + ")"); } } private String checkResourceType(String type) throws EOperationOutcome, Exception { if (context.fetchResource(StructureDefinition.class, "http://hl7.org/fhir/StructureDefinition/" + type) != null) return type; else return null; } private void checkSampledData(List<ValidationMessage> errors, String path, WrapperElement focus, SampledData fixed) { checkFixedValue(errors, path + ".origin", focus.getNamedChild("origin"), fixed.getOrigin(), "origin"); checkFixedValue(errors, path + ".period", focus.getNamedChild("period"), fixed.getPeriodElement(), "period"); checkFixedValue(errors, path + ".factor", focus.getNamedChild("factor"), fixed.getFactorElement(), "factor"); checkFixedValue(errors, path + ".lowerLimit", focus.getNamedChild("lowerLimit"), fixed.getLowerLimitElement(), "lowerLimit"); checkFixedValue(errors, path + ".upperLimit", focus.getNamedChild("upperLimit"), fixed.getUpperLimitElement(), "upperLimit"); checkFixedValue(errors, path + ".dimensions", focus.getNamedChild("dimensions"), fixed.getDimensionsElement(), "dimensions"); checkFixedValue(errors, path + ".data", focus.getNamedChild("data"), fixed.getDataElement(), "data"); } private void checkTiming(List<ValidationMessage> errors, String path, WrapperElement focus, Timing fixed) { checkFixedValue(errors, path + ".repeat", focus.getNamedChild("repeat"), fixed.getRepeat(), "value"); List<WrapperElement> events = new ArrayList<WrapperElement>(); 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"); } } 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(); } private boolean empty(WrapperElement element) { if (element.hasAttribute("value")) return false; if (element.hasAttribute("xml:id")) return false; WrapperElement child = element.getFirstChild(); while (child != null) { if (!child.isXml() || FormatUtilities.FHIR_NS.equals(child.getNamespace())) { return false; } child = child.getNextSibling(); } return true; } private ElementDefinition findElement(StructureDefinition profile, String name) { for (ElementDefinition c : profile.getSnapshot().getElement()) { if (c.getPath().equals(name)) { return c; } } return null; } private String genFullUrl(String bundleBase, String entryBase, String type, String id) { String base = Utilities.noString(entryBase) ? bundleBase : entryBase; if (Utilities.noString(base)) { return type + "/" + id; } else if ("urn:uuid".equals(base) || "urn:oid".equals(base)) return base + id; else return Utilities.appendSlash(base) + type + "/" + id; } public BestPracticeWarningLevel getBasePracticeWarningLevel() { return bpWarnings; } private String getBaseType(StructureDefinition profile, String pr) throws EOperationOutcome, Exception { // if (pr.startsWith("http://hl7.org/fhir/StructureDefinition/")) { // // this just has to be a base type // return pr.substring(40); // } else { StructureDefinition p = resolveProfile(profile, pr); if (p == null) return null; else if (p.getKind() == StructureDefinitionKind.RESOURCE) return p.getSnapshot().getElement().get(0).getPath(); else return p.getSnapshot().getElement().get(0).getType().get(0).getCode(); // } } @Override public CheckDisplayOption getCheckDisplay() { return checkDisplay; } // private String findProfileTag(WrapperElement element) { // String uri = null; // List<WrapperElement> list = new ArrayList<WrapperElement>(); // element.getNamedChildren("category", list); // for (WrapperElement 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(ValueSet vs, String code) { for (ConceptDefinitionComponent c : vs.getCodeSystem().getConcept()) { ConceptDefinitionComponent r = getCodeDefinition(c, code); if (r != null) return r; } return null; } private WrapperElement getContainedById(WrapperElement container, String id) { List<WrapperElement> contained = new ArrayList<WrapperElement>(); container.getNamedChildren("contained", contained); for (WrapperElement we : contained) { WrapperElement res = we.isXml() ? we.getFirstChild() : we; if (id.equals(res.getNamedChildValue("id"))) return res; } return null; } public IWorkerContext getContext() { return context; } private ElementDefinition getCriteriaForDiscriminator(String path, ElementDefinition ed, String discriminator, StructureDefinition profile, List<ValidationMessage> errors) throws Exception { List<ElementDefinition> childDefinitions = ProfileUtilities.getChildMap(profile, ed); List<ElementDefinition> snapshot = null; if (childDefinitions.isEmpty()) { // going to look at the type if (ed.getType().size() == 0) throw new Exception("Error in profile for " + path + " no children, no type"); if (ed.getType().size() > 1) throw new Exception("Error in profile for " + path + " multiple types defined in slice discriminator"); String url; if (ed.getType().get(0).hasProfile()) { url = ed.getType().get(0).getProfile().get(0).getValue(); } else { url = "http://hl7.org/fhir/StructureDefinition/" + ed.getType().get(0).getCode(); } StructureDefinition type = context.fetchResource(StructureDefinition.class, url); if (type == null) { super.fail(errors, IssueType.INCOMPLETE, path, false, "Failed to retrieve StructureDefinition with URL: " + url); return null; } snapshot = type.getSnapshot().getElement(); ed = snapshot.get(0); } else { snapshot = profile.getSnapshot().getElement(); } String originalPath = ed.getPath(); String goal = originalPath + "." + discriminator; int index = snapshot.indexOf(ed); assert(index > -1); 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); } private WrapperElement getExtensionByUrl(List<WrapperElement> extensions, String urlSimple) { for (WrapperElement e : extensions) { if (urlSimple.equals(e.getNamedChildValue("url"))) return e; } return null; } public List<String> getExtensionDomains() { return extensionDomains; } private WrapperElement getFromBundle(WrapperElement bundle, String ref) { List<WrapperElement> entries = new ArrayList<WrapperElement>(); bundle.getNamedChildren("entry", entries); for (WrapperElement we : entries) { WrapperElement res = we.getNamedChild("resource").getFirstChild(); if (res != null) { String url = genFullUrl(bundle.getNamedChildValue("base"), we.getNamedChildValue("base"), res.getName(), res.getNamedChildValue("id")); if (url.endsWith(ref)) return res; } } return null; } private StructureDefinition getProfileForType(ElementDefinition ed, String type) throws Exception { //return context.fetchResource(StructureDefinition.class, "http://hl7.org/fhir/StructureDefinition/" + type); final String url; if (!"Reference".equals(type) && ed.getType().get(0).hasProfile()) { url = ed.getType().get(0).getProfile().get(0).getValue(); } else { url = "http://hl7.org/fhir/StructureDefinition/" + type; } return context.fetchResource(StructureDefinition.class, url); } private Element getValueForDiscriminator(WrapperElement element, String discriminator, ElementDefinition criteria) { throw new Error("validation of slices not done yet"); } private ValueSet getValueSet(String system) throws Exception { return context.fetchCodeSystem(system); } 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:"); } public boolean isAnyExtensionsAllowed() { return anyExtensionsAllowed; } private boolean isBundleEntry(String path) { String[] parts = path.split("\\/"); if (path.startsWith("/f:")) return parts.length > 2 && parts[parts.length - 1].startsWith("f:resource") && (parts[parts.length - 2].equals("f:entry") || parts[parts.length - 2].startsWith("f:entry[")); else return parts.length > 2 && parts[parts.length - 1].equals("resource") && ((parts.length > 2 && parts[parts.length - 3].equals("entry")) || parts[parts.length - 2].equals("entry")); } private boolean isParametersEntry(String path) { String[] parts = path.split("\\/"); if (path.startsWith("/f:")) return parts.length == 4 && parts[parts.length-3].equals("f:Parameters") && parts[parts.length-2].startsWith("f:parameter") && parts[parts.length-1].startsWith("f:resource"); else return parts.length == 4 && parts[parts.length-3].equals("Parameters") && parts[parts.length-2].startsWith("parameter") && parts[parts.length-1].startsWith("resource"); } private boolean isPrimitiveType(String type) { return type.equalsIgnoreCase("boolean") || type.equalsIgnoreCase("integer") || type.equalsIgnoreCase("string") || type.equalsIgnoreCase("decimal") || type.equalsIgnoreCase("uri") || type.equalsIgnoreCase("base64Binary") || type.equalsIgnoreCase("instant") || type.equalsIgnoreCase("date") || type.equalsIgnoreCase("uuid") || type.equalsIgnoreCase("id") || type.equalsIgnoreCase("xhtml") || type.equalsIgnoreCase("markdown") || type.equalsIgnoreCase("dateTime") || type.equalsIgnoreCase("time") || type.equalsIgnoreCase("code") || type.equalsIgnoreCase("oid") || type.equalsIgnoreCase("id"); } public boolean isRequireResourceId() { return requiresResourceId; } 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 WrapperElement resolve(String ref, NodeStack stack) { 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) { WrapperElement res = getContainedById(stack.getElement(), ref.substring(1)); if (res != null) return res; 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 while (stack != null && stack.getElement() != null) { if ("Bundle".equals(stack.getElement().getResourceType())) { WrapperElement res = getFromBundle(stack.getElement(), ref.substring(1)); if (res != null) return res; } stack = stack.parent; } // todo: consult the external host for resolution return null; } } private ValueSet resolveBindingReference(Type reference) throws EOperationOutcome, Exception { if (reference instanceof UriType) return context.fetchResource(ValueSet.class, ((UriType) reference).getValue().toString()); else if (reference instanceof Reference) return context.fetchResource(ValueSet.class, ((Reference) reference).getReference()); else return null; } private WrapperElement resolveInBundle(List<WrapperElement> 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 (WrapperElement 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 (WrapperElement entry : entries) { String fu = entry.getNamedChildValue("fullUrl"); if (u != null && fullUrl.equals(u)) return entry; if (u == null) { WrapperElement res = entry.getNamedChild("resource"); WrapperElement resource = res.getFirstChild(); String et = resource.getResourceType(); String eid = resource.getNamedChildValue("id"); if (t.equals(et) && i.equals(eid)) return entry; } } } return null; } } private ElementDefinition resolveNameReference(StructureDefinitionSnapshotComponent snapshot, String name) { for (ElementDefinition ed : snapshot.getElement()) if (name.equals(ed.getName())) return ed; return null; } private StructureDefinition resolveProfile(StructureDefinition profile, String pr) throws EOperationOutcome, Exception { if (pr.startsWith("#")) { for (Resource r : profile.getContained()) { if (r.getId().equals(pr.substring(1)) && r instanceof StructureDefinition) return (StructureDefinition) r; } return null; } else return context.fetchResource(StructureDefinition.class, pr); } private ElementDefinition resolveType(ElementDefinition ed, String type) throws EOperationOutcome, Exception { final String url; if (ed.getType().get(0).hasProfile()) { url = ed.getType().get(0).getProfile().get(0).getValue(); } else { url = "http://hl7.org/fhir/StructureDefinition/" + type; } StructureDefinition sd = context.fetchResource(StructureDefinition.class, url); if (sd == null || !sd.hasSnapshot()) return null; else return sd.getSnapshot().getElement().get(0); } public void setAnyExtensionsAllowed(boolean anyExtensionsAllowed) { this.anyExtensionsAllowed = anyExtensionsAllowed; } public void setBestPracticeWarningLevel(BestPracticeWarningLevel value) { bpWarnings = value; } @Override public void setCheckDisplay(CheckDisplayOption checkDisplay) { this.checkDisplay = checkDisplay; } public void setRequireResourceId(boolean requiresResourceId) { this.requiresResourceId = requiresResourceId; } public void setSuppressLoincSnomedMessages(boolean suppressLoincSnomedMessages) { this.suppressLoincSnomedMessages = suppressLoincSnomedMessages; } /** * * @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 * @return * @throws Exception */ private boolean sliceMatches(WrapperElement element, String path, ElementDefinition slice, ElementDefinition ed, StructureDefinition profile, List<ValidationMessage> errors) throws Exception { if (!slice.getSlicing().hasDiscriminator()) return false; // cannot validate in this case for (StringType s : slice.getSlicing().getDiscriminator()) { String discriminator = s.getValue(); ElementDefinition criteria = getCriteriaForDiscriminator(path, ed, discriminator, profile, errors); if (criteria == null) { return false; } if (discriminator.equals("url") && criteria.getPath().equals("Extension.url")) { if (!element.getAttribute("url").equals(((UriType) criteria.getFixed()).asStringValue())) return false; } else { Element value = getValueForDiscriminator(element, discriminator, criteria); if (!valueMatchesCriteria(value, criteria)) return false; } } return true; } // 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(List<ValidationMessage> errors, WrapperElement element, StructureDefinition profile, NodeStack stack) throws Exception { // profile is valid, and matches the resource name if (rule(errors, IssueType.STRUCTURE, element.line(), element.col(), stack.getLiteralPath(), profile.hasSnapshot(), "StructureDefinition has no snapshot - validation is against the snapshot, so it must be provided")) { validateElement(errors, profile, profile.getSnapshot().getElement().get(0), null, null, element, element.getName(), stack); checkDeclaredProfiles(errors, element, stack); // specific known special validations if (element.getResourceType().equals("Bundle")) validateBundle(errors, element, stack); if (element.getResourceType().equals("Observation")) validateObservation(errors, element, stack); } } private String tail(String path) { return path.substring(path.lastIndexOf(".") + 1); } private String tryParse(String ref) throws EOperationOutcome, Exception { 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; } @Override public List<ValidationMessage> validate(Document document) throws Exception { List<ValidationMessage> results = new ArrayList<ValidationMessage>(); validate(results, document); return results; } @Override public List<ValidationMessage> validate(Document document, String profile) throws Exception { List<ValidationMessage> results = new ArrayList<ValidationMessage>(); validate(results, document, profile); return results; } @Override public List<ValidationMessage> validate(Document document, StructureDefinition profile) throws Exception { List<ValidationMessage> results = new ArrayList<ValidationMessage>(); validate(results, document, profile); return results; } @Override public List<ValidationMessage> validate(Element element) throws Exception { List<ValidationMessage> results = new ArrayList<ValidationMessage>(); validate(results, element); return results; } @Override public List<ValidationMessage> validate(Element element, String profile) throws Exception { List<ValidationMessage> results = new ArrayList<ValidationMessage>(); validate(results, element, profile); return results; } @Override public List<ValidationMessage> validate(Element element, StructureDefinition profile) throws Exception { List<ValidationMessage> results = new ArrayList<ValidationMessage>(); validate(results, element, profile); return results; } @Override public List<ValidationMessage> validate(JsonObject obj) throws Exception { List<ValidationMessage> results = new ArrayList<ValidationMessage>(); validate(results, obj); return results; } @Override public List<ValidationMessage> validate(JsonObject obj, String profile) throws Exception { List<ValidationMessage> results = new ArrayList<ValidationMessage>(); validate(results, obj, profile); return results; } @Override public List<ValidationMessage> validate(JsonObject obj, StructureDefinition profile) throws Exception { List<ValidationMessage> results = new ArrayList<ValidationMessage>(); validate(results, obj, profile); return results; } @Override public void validate(List<ValidationMessage> errors, Document document) throws Exception { checkForProcessingInstruction(errors, document); validateResource(errors, new DOMWrapperElement(document.getDocumentElement()), null, requiresResourceId, null); } @Override public void validate(List<ValidationMessage> errors, Document document, String profile) throws Exception { checkForProcessingInstruction(errors, document); StructureDefinition p = context.fetchResource(StructureDefinition.class, profile); if (p == null) throw new Exception("StructureDefinition '" + profile + "' not found"); validateResource(errors, new DOMWrapperElement(document.getDocumentElement()), p, requiresResourceId, null); } @Override public void validate(List<ValidationMessage> errors, Document document, StructureDefinition profile) throws Exception { checkForProcessingInstruction(errors, document); validateResource(errors, new DOMWrapperElement(document.getDocumentElement()), profile, requiresResourceId, null); } @Override public void validate(List<ValidationMessage> errors, Element element) throws Exception { validateResource(errors, new DOMWrapperElement(element), null, requiresResourceId, null); } @Override public void validate(List<ValidationMessage> errors, Element element, String profile) throws Exception { StructureDefinition p = context.fetchResource(StructureDefinition.class, profile); if (p == null) throw new Exception("StructureDefinition '" + profile + "' not found"); validateResource(errors, new DOMWrapperElement(element), p, requiresResourceId, null); } @Override public void validate(List<ValidationMessage> errors, Element element, StructureDefinition profile) throws Exception { validateResource(errors, new DOMWrapperElement(element), profile, requiresResourceId, null); } @Override public void validate(List<ValidationMessage> errors, JsonObject object) throws Exception { validateResource(errors, new JsonWrapperElement(object), null, requiresResourceId, null); } @Override public void validate(List<ValidationMessage> errors, JsonObject object, String profile) throws Exception { StructureDefinition p = context.fetchResource(StructureDefinition.class, profile); if (p == null) throw new Exception("StructureDefinition '" + profile + "' not found"); validateResource(errors, new JsonWrapperElement(object), p, requiresResourceId, null); } @Override public void validate(List<ValidationMessage> errors, JsonObject object, StructureDefinition profile) throws Exception { validateResource(errors, new JsonWrapperElement(object), profile, requiresResourceId, null); } private void validateBundle(List<ValidationMessage> errors, WrapperElement bundle, NodeStack stack) { List<WrapperElement> entries = new ArrayList<WrapperElement>(); 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 { WrapperElement firstEntry = entries.get(0); NodeStack firstStack = stack.push(firstEntry, 0, null, null); String fullUrl = firstEntry.getNamedChildValue("fullUrl"); if (type.equals("document")) { WrapperElement res = firstEntry.getNamedChild("resource"); NodeStack localStack = firstStack.push(res, -1, null, null); WrapperElement resource = res.getFirstChild(); String id = resource.getNamedChildValue("id"); if (rule(errors, IssueType.INVALID, firstEntry.line(), firstEntry.col(), stack.addToLiteralPath("entry", ":0"), res != null, "No resource on first entry")) { if (bundle.isXml()) validateDocument(errors, entries, resource, localStack.push(resource, -1, null, null), fullUrl, id); else validateDocument(errors, entries, res, localStack, fullUrl, id); } } if (type.equals("message")) validateMessage(errors, bundle); } } // public class ProfileStructureIterator { // // private StructureDefinition profile; // private ElementDefinition elementDefn; // private List<String> names = new ArrayList<String>(); // private Map<String, List<ElementDefinition>> children = new HashMap<String, List<ElementDefinition>>(); // private int cursor; // // public ProfileStructureIterator(StructureDefinition profile, ElementDefinition elementDefn) { // this.profile = profile; // this.elementDefn = elementDefn; // loadMap(); // cursor = -1; // } // // private void loadMap() { // int i = profile.getSnapshot().getElement().indexOf(elementDefn) + 1; // String lead = elementDefn.getPath(); // while (i < profile.getSnapshot().getElement().size()) { // String name = profile.getSnapshot().getElement().get(i).getPath(); // if (name.length() <= lead.length()) // return; // cause we've got to the end of the possible matches // String tail = name.substring(lead.length()+1); // if (Utilities.isToken(tail) && name.substring(0, lead.length()).equals(lead)) { // List<ElementDefinition> list = children.get(tail); // if (list == null) { // list = new ArrayList<ElementDefinition>(); // names.add(tail); // children.put(tail, list); // } // list.add(profile.getSnapshot().getElement().get(i)); // } // i++; // } // } // // public boolean more() { // cursor++; // return cursor < names.size(); // } // // public List<ElementDefinition> current() { // return children.get(name()); // } // // public String name() { // return names.get(cursor); // } // // } // // private void checkByProfile(List<ValidationMessage> errors, String path, WrapperElement focus, StructureDefinition profile, ElementDefinition elementDefn) // throws Exception { // // we have an element, and the structure that describes it. // // we know that's it's valid against the underlying spec - is it valid against this one? // // in the instance validator above, we assume that schema or schmeatron has taken care of cardinalities, but here, we have no such reliance. // // so the walking algorithm is different: we're going to walk the definitions // String type; // if (elementDefn.getPath().endsWith("[x]")) { // String tail = elementDefn.getPath().substring(elementDefn.getPath().lastIndexOf(".")+1, elementDefn.getPath().length()-3); // type = focus.getName().substring(tail.length()); // rule(errors, IssueType.STRUCTURE, focus.line(), focus.col(), path, typeAllowed(type, elementDefn.getType()), "The type '"+type+"' is not allowed at this // point (must be one of '"+typeSummary(elementDefn)+")"); // } else { // if (elementDefn.getType().size() == 1) { // type = elementDefn.getType().size() == 0 ? null : elementDefn.getType().get(0).getCode(); // } else // type = null; // } // // constraints: // for (ElementDefinitionConstraintComponent c : elementDefn.getConstraint()) // checkConstraint(errors, path, focus, c); // if (elementDefn.hasBinding() && type != null) // checkBinding(errors, path, focus, profile, elementDefn, type); // // // type specific checking: // if (type != null && isPrimitiveType(type)) { // checkPrimitiveByProfile(errors, path, focus, elementDefn); // } else { // if (elementDefn.hasFixed()) // checkFixedValue(errors, path, focus, elementDefn.getFixed(), ""); // // ProfileStructureIterator walker = new ProfileStructureIterator(profile, elementDefn); // while (walker.more()) { // // collect all the slices for the path // List<ElementDefinition> childset = walker.current(); // // collect all the elements that match it by name // List<WrapperElement> children = new ArrayList<WrapperElement>(); // focus.getNamedChildrenWithWildcard(walker.name(), children); // // if (children.size() == 0) { // // well, there's no children - should there be? // for (ElementDefinition defn : childset) { // if (!rule(errors, IssueType.REQUIRED, focus.line(), focus.col(), path, defn.getMin() == 0, "Required Element '"+walker.name()+"' missing")) // break; // no point complaining about missing ones after the first one // } // } else if (childset.size() == 1) { // // simple case: one possible definition, and one or more children. // rule(errors, IssueType.STRUCTURE, focus.line(), focus.col(), path, childset.get(0).getMax().equals("*") || Integer.parseInt(childset.get(0).getMax()) >= // children.size(), // "Too many elements for '"+walker.name()+"'"); // todo: sort out structure // for (WrapperElement child : children) { // checkByProfile(errors, childset.get(0).getPath(), child, profile, childset.get(0)); // } // } else { // // ok, this is the full case - we have a list of definitions, and a list of candidates for meeting those definitions. // // we need to decide *if* that match a given definition // } // } // } // } // private void checkBinding(List<ValidationMessage> errors, String path, WrapperElement focus, StructureDefinition profile, ElementDefinition elementDefn, // String type) throws EOperationOutcome, Exception { // ElementDefinitionBindingComponent bc = elementDefn.getBinding(); // // if (bc != null && bc.hasValueSet() && bc.getValueSet() instanceof Reference) { // String url = ((Reference) bc.getValueSet()).getReference(); // ValueSet vs = resolveValueSetReference(profile, (Reference) bc.getValueSet()); // if (vs == null) { // rule(errors, IssueType.STRUCTURE, focus.line(), focus.col(), path, false, "Cannot check binding on type '"+type+"' as the value set '"+url+"' could not be // located"); // } else if (type.equals("code")) // checkBindingCode(errors, path, focus, vs); // else if (type.equals("Coding")) // checkBindingCoding(errors, path, focus, vs); // else if (type.equals("CodeableConcept")) // checkBindingCodeableConcept(errors, path, focus, vs); // else // rule(errors, IssueType.STRUCTURE, focus.line(), focus.col(), path, false, "Cannot check binding on type '"+type+"'"); // } // } // // private ValueSet resolveValueSetReference(StructureDefinition profile, Reference reference) throws EOperationOutcome, Exception { // if (reference.getReference().startsWith("#")) { // for (Resource r : profile.getContained()) { // if (r instanceof ValueSet && r.getId().equals(reference.getReference().substring(1))) // return (ValueSet) r; // } // return null; // } else // return resolveBindingReference(reference); // // } // // private void checkBindingCode(List<ValidationMessage> errors, String path, WrapperElement focus, ValueSet vs) { // // rule(errors, "exception", path, false, "checkBindingCode not done yet"); // } // // private void checkBindingCoding(List<ValidationMessage> errors, String path, WrapperElement focus, ValueSet vs) { // // rule(errors, "exception", path, false, "checkBindingCoding not done yet"); // } // // private void checkBindingCodeableConcept(List<ValidationMessage> errors, String path, WrapperElement focus, ValueSet vs) { // // rule(errors, "exception", path, false, "checkBindingCodeableConcept not done yet"); // } // // private String typeSummary(ElementDefinition elementDefn) { // StringBuilder b = new StringBuilder(); // for (TypeRefComponent t : elementDefn.getType()) { // b.append("|"+t.getCode()); // } // return b.toString().substring(1); // } // // private boolean typeAllowed(String t, List<TypeRefComponent> types) { // for (TypeRefComponent type : types) { // if (t.equals(Utilities.capitalize(type.getCode()))) // return true; // if (t.equals("Resource") && Utilities.capitalize(type.getCode()).equals("Reference")) // return true; // } // return false; // } // // private void checkConstraint(List<ValidationMessage> errors, String path, WrapperElement focus, ElementDefinitionConstraintComponent c) throws Exception { // //// try //// { //// XPathFactory xpf = new net.sf.saxon.xpath.XPathFactoryImpl(); //// NamespaceContext context = new NamespaceContextMap("f", "http://hl7.org/fhir", "h", "http://www.w3.org/1999/xhtml"); //// //// XPath xpath = xpf.newXPath(); //// xpath.setNamespaceContext(context); //// Boolean ok = (Boolean) xpath.evaluate(c.getXpath(), focus, XPathConstants.BOOLEAN); //// if (ok == null || !ok) { //// if (c.getSeverity() == ConstraintSeverity.warning) //// warning(errors, "invariant", path, false, c.getHuman()); //// else //// rule(errors, "invariant", path, false, c.getHuman()); //// } //// } //// catch (XPathExpressionException e) { //// rule(errors, "invariant", path, false, "error executing invariant: "+e.getMessage()); //// } // } // // private void checkPrimitiveByProfile(List<ValidationMessage> errors, String path, WrapperElement focus, ElementDefinition elementDefn) { // // two things to check - length, and fixed value // String value = focus.getAttribute("value"); // if (elementDefn.hasMaxLengthElement()) { // rule(errors, IssueType.TOOLONG, focus.line(), focus.col(), path, value.length() <= elementDefn.getMaxLength(), "The value '"+value+"' exceeds the allow // length limit of "+Integer.toString(elementDefn.getMaxLength())); // } // if (elementDefn.hasFixed()) { // checkFixedValue(errors, path, focus, elementDefn.getFixed(), ""); // } // } private void validateBundleReference(List<ValidationMessage> errors, List<WrapperElement> entries, WrapperElement ref, String name, NodeStack stack, String fullUrl, String type, String id) { if (ref != null && !Utilities.noString(ref.getNamedChildValue("reference"))) { WrapperElement target = resolveInBundle(entries, ref.getNamedChildValue("reference"), fullUrl, type, id); rule(errors, IssueType.INVALID, target.line(), target.col(), stack.addToLiteralPath("reference"), target != null, "Unable to resolve the target of the reference in the bundle (" + name + ")"); } } private void validateContains(List<ValidationMessage> errors, String path, ElementDefinition child, ElementDefinition context, WrapperElement element, NodeStack stack, boolean needsId) throws Exception { WrapperElement e = element.isXml() ? element.getFirstChild() : element; String resourceName = e.getResourceType(); StructureDefinition profile = this.context.fetchResource(StructureDefinition.class, "http://hl7.org/fhir/StructureDefinition/" + resourceName); if (rule(errors, IssueType.INVALID, element.line(), element.col(), stack.addToLiteralPath(resourceName), profile != null, "No profile found for contained resource of type '" + resourceName + "'")) validateResource(errors, e, profile, needsId, stack); } private void validateDocument(List<ValidationMessage> errors, List<WrapperElement> entries, WrapperElement 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.getResourceType().equals("Composition"), "The first entry in a document must be a composition")) { // the composition subject and section references must resolve in the bundle validateBundleReference(errors, entries, composition.getNamedChild("subject"), "Composition Subject", stack.push(composition.getNamedChild("subject"), -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(List<ValidationMessage> errors, StructureDefinition profile, ElementDefinition definition, StructureDefinition cprofile, ElementDefinition context, WrapperElement element, String actualType, NodeStack stack) throws Exception { // irrespective of what element it is, it cannot be empty if (element.isXml()) { rule(errors, IssueType.INVALID, element.line(), element.col(), stack.getLiteralPath(), FormatUtilities.FHIR_NS.equals(element.getNamespace()), "Namespace mismatch - expected '" + FormatUtilities.FHIR_NS + "', found '" + element.getNamespace() + "'"); rule(errors, IssueType.INVALID, element.line(), element.col(), stack.getLiteralPath(), !element.hasNamespace("http://www.w3.org/2001/XMLSchema-instance"), "Schema Instance Namespace is not allowed in instances"); rule(errors, IssueType.INVALID, element.line(), element.col(), stack.getLiteralPath(), !element.hasProcessingInstruction(), "No Processing Instructions in resources"); } rule(errors, IssueType.INVALID, element.line(), element.col(), stack.getLiteralPath(), !empty(element), "Elements must have some content (@value, extensions, or children elements)"); // get the list of direct defined children, including slices List<ElementDefinition> childDefinitions = ProfileUtilities.getChildMap(profile, definition.getName(), definition.getPath(), definition.getNameReference()); // 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 slice = null; for (ElementDefinition ed : childDefinitions) { boolean process = true; // where are we with slicing if (ed.hasSlicing()) { if (slice != null && slice.getPath().equals(ed.getPath())) throw new Exception("Slice encountered midway through path on " + slice.getPath()); slice = ed; process = false; } else if (slice != null && !slice.getPath().equals(ed.getPath())) slice = null; if (process) { for (ElementInfo ei : children) { boolean match = false; if (slice == null) { match = nameMatches(ei.name, tail(ed.getPath())); } else { if (nameMatches(ei.name, tail(ed.getPath()))) match = sliceMatches(ei.element, ei.path, slice, ed, profile, errors); } if (match) { if (rule(errors, IssueType.INVALID, ei.line(), ei.col(), ei.path, ei.definition == null, "Element matches more than one slice")) ei.definition = ed; } } } } for (ElementInfo ei : children) 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.getAttribute("url") + "\")"); else rule(errors, IssueType.INVALID, ei.line(), ei.col(), ei.path, (ei.definition != null) || (!ei.element.isXml() && ei.element.getName().equals("fhir_comments")), "Element is unknown or does not match any slice"); // 3. report any definitions that have a cardinality problem for (ElementDefinition ed : childDefinitions) { if (ed.getRepresentation().isEmpty()) { // ignore xml attributes int count = 0; for (ElementInfo ei : children) if (ei.definition == ed) count++; if (ed.getMin() > 0) { rule(errors, IssueType.STRUCTURE, element.line(), element.col(), stack.getLiteralPath(), count >= ed.getMin(), "Element '" + stack.getLiteralPath() + "." + tail(ed.getPath()) + "': minimum required = " + Integer.toString(ed.getMin()) + ", but only found " + Integer.toString(count)); } if (ed.hasMax() && !ed.getMax().equals("*")) { rule(errors, IssueType.STRUCTURE, element.line(), element.col(), stack.getLiteralPath(), count <= Integer.parseInt(ed.getMax()), "Element " + tail(ed.getPath()) + " @ " + stack.getLiteralPath() + ": max allowed = " + ed.getMax() + ", but found " + Integer.toString(count)); } } } // 4. check order if any slices are orderd. (todo) // 5. inspect each child for validity for (ElementInfo ei : children) { 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(); 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); } 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(); 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.getNameReference() != null) { typeDefn = resolveNameReference(profile.getSnapshot(), ei.definition.getNameReference()); } 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(ei.definition, type)); String localStackLiterapPath = localStack.getLiteralPath(); String eiPath = ei.path; assert(eiPath.equals(localStackLiterapPath)) : "ei.path: " + ei.path + " - localStack.getLiterapPath: " + localStackLiterapPath; if (type != null) { if (isPrimitiveType(type)) checkPrimitive(errors, ei.path, type, ei.definition, ei.element); 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); } else if (type.equals("CodeableConcept")) checkCodeableConcept(errors, ei.path, ei.element, profile, ei.definition); else if (type.equals("Reference")) checkReference(errors, ei.path, ei.element, profile, ei.definition, actualType, localStack); if (type.equals("Extension")) checkExtension(errors, ei.path, ei.element, ei.definition, profile, localStack); else if (type.equals("Resource")) validateContains(errors, ei.path, ei.definition, definition, ei.element, localStack, !isBundleEntry(ei.path) && !isParametersEntry(ei.path) && shouldCheckForIdPresence); // if // (str.matches(".*([.,/])work\\1$")) else { StructureDefinition p = getProfileForType(ei.definition, type); if (rule(errors, IssueType.STRUCTURE, ei.line(), ei.col(), ei.path, p != null, "Unknown type " + type)) { validateElement(errors, p, p.getSnapshot().getElement().get(0), profile, ei.definition, ei.element, type, localStack); } } } } else { if (rule(errors, IssueType.STRUCTURE, ei.line(), ei.col(), stack.getLiteralPath(), ei.definition != null, "Unrecognised Content " + ei.name)) validateElement(errors, profile, ei.definition, null, null, ei.element, type, localStack); } } } } private void validateMessage(List<ValidationMessage> errors, WrapperElement bundle) { // TODO Auto-generated method stub } private void validateObservation(List<ValidationMessage> errors, WrapperElement 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, WrapperElement element, StructureDefinition profile, boolean needsId, NodeStack stack) throws Exception { if (stack == null) stack = new NodeStack(element.isXml()); // getting going - either we got a profile, or not. boolean ok = true; if (element.isXml()) { ok = rule(errors, IssueType.INVALID, element.line(), element.col(), "/", element.getNamespace().equals(FormatUtilities.FHIR_NS), "Namespace mismatch - expected '" + FormatUtilities.FHIR_NS + "', found '" + element.getNamespace() + "'"); } if (ok) { String resourceName = element.getResourceType(); if (profile == null) { profile = context.fetchResource(StructureDefinition.class, "http://hl7.org/fhir/StructureDefinition/" + resourceName); ok = rule(errors, IssueType.INVALID, element.line(), element.col(), stack.addToLiteralPath(resourceName), profile != null, "No profile found for resource type '" + resourceName + "'"); } else { String type = profile.hasConstrainedType() ? profile.getConstrainedType() : profile.getName(); ok = rule(errors, IssueType.INVALID, -1, -1, stack.addToLiteralPath(resourceName), type.equals(resourceName), "Specified profile type was '" + profile.getConstrainedType() + "', but resource type was '" + resourceName + "'"); } } if (ok) { stack = stack.push(element, -1, profile.getSnapshot().getElement().get(0), profile.getSnapshot().getElement().get(0)); if (needsId && (element.getNamedChild("id") == null)) rule(errors, IssueType.INVALID, element.line(), element.col(), stack.getLiteralPath(), false, "Resource has no id"); start(errors, element, profile, stack); // root is both definition and type } } private void validateSections(List<ValidationMessage> errors, List<WrapperElement> entries, WrapperElement focus, NodeStack stack, String fullUrl, String id) { List<WrapperElement> sections = new ArrayList<WrapperElement>(); focus.getNamedChildren("entry", sections); int i = 0; for (WrapperElement 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) { throw new Error("validation of slices not done yet"); } 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 WrapperElement child; private int lastCount; private WrapperElement parent; public ChildIterator(String path, WrapperElement element) { parent = element; basePath = path; } public int count() { WrapperElement n = child.getNextSibling(); if (n != null && n.getName().equals(child.getName())) { return lastCount + 1; } else return -1; } public WrapperElement element() { return child; } public String name() { return child.getName(); } public boolean next() { if (child == null) { child = parent.getFirstChild(); lastCount = 0; } else { String lastName = child.getName(); child = child.getNextSibling(); if (child != null && child.getName().equals(lastName)) lastCount++; else lastCount = 0; } return child != null; } public String path() { WrapperElement n = child.getNextSibling(); if (parent.isXml()) { String sfx = ""; if (n != null && n.getName().equals(child.getName())) { sfx = "[" + Integer.toString(lastCount + 1) + "]"; } if (FormatUtilities.XHTML_NS.equals(child.getNamespace())) return basePath + "/h:" + name() + sfx; else return basePath + "/f:" + name() + sfx; } else { String sfx = ""; if (n != null && n.getName().equals(child.getName())) { sfx = "/" + Integer.toString(lastCount + 1); } return basePath + "/" + name() + sfx; } } } public class DOMWrapperElement extends WrapperElement { private int col; private Element element; private int line; public DOMWrapperElement(Element element) { super(); this.element = element; XmlLocationData loc = (XmlLocationData) element.getUserData(XmlLocationData.LOCATION_DATA_KEY); if (loc != null) { line = loc.getStartLine(); col = loc.getStartColumn(); } else { line = -1; col = -1; } } @Override public int col() { return col; } @Override public String getAttribute(String name) { return element.getAttribute(name); } @Override public WrapperElement getFirstChild() { Element res = XMLUtil.getFirstChild(element); return res == null ? null : new DOMWrapperElement(res); } @Override public String getName() { return element.getLocalName(); } @Override public WrapperElement getNamedChild(String name) { Element res = XMLUtil.getNamedChild(element, name); return res == null ? null : new DOMWrapperElement(res); } @Override public void getNamedChildren(String name, List<WrapperElement> list) { List<Element> el = new ArrayList<Element>(); XMLUtil.getNamedChildren(element, name, el); for (Element e : el) list.add(new DOMWrapperElement(e)); } @Override public void getNamedChildrenWithWildcard(String name, List<WrapperElement> list) { List<Element> el = new ArrayList<Element>(); XMLUtil.getNamedChildrenWithWildcard(element, name, el); for (Element e : el) list.add(new DOMWrapperElement(e)); } @Override public String getNamedChildValue(String name) { return XMLUtil.getNamedChildValue(element, name); } @Override public String getNamespace() { return element.getNamespaceURI(); } @Override public WrapperElement getNextSibling() { Element res = XMLUtil.getNextSibling(element); return res == null ? null : new DOMWrapperElement(res); } @Override public String getResourceType() { return element.getLocalName(); } @Override public String getText() { return element.getTextContent(); } @Override public boolean hasAttribute(String name) { return element.hasAttribute(name); } @Override public boolean hasNamespace(String ns) { for (int i = 0; i < element.getAttributes().getLength(); i++) { Node a = element.getAttributes().item(i); if ((a.getNodeName().equals("xmlns") || a.getNodeName().startsWith("xmlns:")) && a.getNodeValue().equals(ns)) return true; } return false; } @Override public boolean hasProcessingInstruction() { Node node = element.getFirstChild(); while (node != null) { if (node.getNodeType() == Node.PROCESSING_INSTRUCTION_NODE) return true; node = node.getNextSibling(); } return false; } @Override public boolean isXml() { return true; } @Override public int line() { return line; } } public class ElementInfo { public int count; public ElementDefinition definition; private WrapperElement element; private String name; private String path; public ElementInfo(String name, WrapperElement 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 class JsonWrapperElement extends WrapperElement { private JsonElement _element; private List<JsonWrapperElement> children = new ArrayList<JsonWrapperElement>(); private JsonElement element; private int index; private String name; private JsonWrapperElement parent; private String path; private String resourceType; public JsonWrapperElement(JsonElement element) { super(); this.name = null; this.resourceType = ((JsonObject) element).get("resourceType").getAsString(); this.element = element; this.path = ""; createChildren(); } public JsonWrapperElement(String path, String name, JsonElement element, JsonElement _element, JsonWrapperElement parent, int index) { super(); this.path = path + "/" + name; this.name = name; this.element = element; if (element instanceof JsonObject && ((JsonObject) element).has("resourceType")) this.resourceType = ((JsonObject) element).get("resourceType").getAsString(); this._element = _element; this.parent = parent; this.index = index; createChildren(); } @Override public int col() { // TODO Auto-generated method stub return -1; } private void createChildren() { // System.out.println(" ..: "+path); // we're going to make this look like the XML if (element == null) throw new Error("not done yet"); if (element instanceof JsonPrimitive) { // we may have an element_ too if (_element != null && _element instanceof JsonObject) for (Entry<String, JsonElement> t : ((JsonObject) _element).entrySet()) processChild(t.getKey(), t.getValue()); } else if (element instanceof JsonObject) { for (Entry<String, JsonElement> t : ((JsonObject) element).entrySet()) if (!t.getKey().equals("resourceType")) { processChild(t.getKey(), t.getValue()); } } else if (element instanceof JsonNull) { // nothing to do } else throw new Error("unexpected condition: " + element.getClass().getName()); } @Override public String getAttribute(String name) { if (name.equals("value")) { if (element == null) return null; if (element instanceof JsonPrimitive) return ((JsonPrimitive) element).getAsString(); return null; } if (name.equals("xml:id")) { WrapperElement c = getNamedChild("id"); return c == null ? null : c.getAttribute("value"); } if (name.equals("url")) { WrapperElement c = getNamedChild("url"); return c == null ? null : c.getAttribute("value"); } throw new Error("not done yet: " + name); } @Override public WrapperElement getFirstChild() { if (children.isEmpty()) return null; else return children.get(0); } @Override public String getName() { return name; } @Override public WrapperElement getNamedChild(String name) { for (JsonWrapperElement j : children) if (j.name.equals(name)) return j; return null; } @Override public void getNamedChildren(String name, List<WrapperElement> list) { for (JsonWrapperElement j : children) if (j.name.equals(name)) list.add(j); } @Override public void getNamedChildrenWithWildcard(String name, List<WrapperElement> list) { throw new Error("not done yet"); } @Override public String getNamedChildValue(String name) { WrapperElement c = getNamedChild(name); return c == null ? null : c.getAttribute("value"); } @Override public String getNamespace() { // return element.getNamespaceURI(); throw new Error("not done yet"); } @Override public WrapperElement getNextSibling() { if (parent == null) return null; if (index >= parent.children.size() - 1) return null; return parent.children.get(index + 1); } @Override public String getResourceType() { return resourceType; } @Override public String getText() { throw new Error("not done yet"); } @Override public boolean hasAttribute(String name) { if (name.equals("value")) { if (element == null) return false; if (element instanceof JsonPrimitive) return true; return false; } if (name.equals("xml:id")) { return getNamedChild("id") != null; } throw new Error("not done yet: " + name); } @Override public boolean hasNamespace(String ns) { throw new Error("not done"); } @Override public boolean hasProcessingInstruction() { return false; } @Override public boolean isXml() { return false; } @Override public int line() { return -1; } private void processChild(String name, JsonElement e) throws Error { if (name.startsWith("_")) { name = name.substring(1); if (((JsonObject) element).has(name)) return; // it will get processed anyway e = null; } JsonElement _e = element instanceof JsonObject ? ((JsonObject) element).get("_" + name) : null; if (e instanceof JsonPrimitive || (e == null && _e != null && !(_e instanceof JsonArray))) { children.add(new JsonWrapperElement(path, name, e, _e, this, children.size())); } else if (e instanceof JsonArray || (e == null && _e != null)) { JsonArray array = (JsonArray) e; JsonArray _array = (JsonArray) _e; int max = array != null ? array.size() : 0; if (_array != null && _array.size() > max) max = _array.size(); for (int i = 0; i < max; i++) { JsonElement a = array == null || array.size() < i ? null : array.get(i); JsonElement _a = _array == null || _array.size() < i ? null : _array.get(i); children.add(new JsonWrapperElement(path, name, a, _a, this, children.size())); } } else if (e instanceof JsonObject) { children.add(new JsonWrapperElement(path, name, e, null, this, children.size())); } else throw new Error("not done yet: " + e.getClass().getName()); } } private class NodeStack { private ElementDefinition definition; private WrapperElement element; private ElementDefinition extension; private String literalPath; // xpath format private List<String> logicalPaths; // dotted format, various entry points private NodeStack parent; private ElementDefinition type; private boolean xml; public NodeStack(boolean xml) { this.xml = xml; } public String addToLiteralPath(String... path) { StringBuilder b = new StringBuilder(); b.append(getLiteralPath()); if (xml) { for (String p : path) { if (p.startsWith(":")) { b.append("["); b.append(p.substring(1)); b.append("]"); } else { b.append("/f:"); b.append(p); } } } else { for (String p : path) { b.append("/"); if (p.startsWith(":")) { b.append(p.substring(1)); } else { b.append(p); } } } return b.toString(); } private ElementDefinition getDefinition() { return definition; } private WrapperElement getElement() { return element; } private 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(WrapperElement element, int count, ElementDefinition definition, ElementDefinition type) { NodeStack res = new NodeStack(element.isXml()); res.parent = this; res.element = element; res.definition = definition; if (element.isXml()) { res.literalPath = getLiteralPath() + (element.getNamespace().equals(FormatUtilities.XHTML_NS) ? "/h:" : "/f:") + element.getName(); if (count > -1) res.literalPath = res.literalPath + "[" + Integer.toString(count) + "]"; } else { if (element.getName() == null) res.literalPath = ""; else 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; } } public abstract class WrapperElement { public abstract int col(); public abstract String getAttribute(String name); public abstract WrapperElement getFirstChild(); public abstract String getName(); public abstract WrapperElement getNamedChild(String name); public abstract void getNamedChildren(String name, List<WrapperElement> list); public abstract void getNamedChildrenWithWildcard(String name, List<WrapperElement> list); public abstract String getNamedChildValue(String name); public abstract String getNamespace(); public abstract WrapperElement getNextSibling(); public abstract String getResourceType(); public abstract String getText(); public abstract boolean hasAttribute(String name); public abstract boolean hasNamespace(String string); public abstract boolean hasProcessingInstruction(); public abstract boolean isXml(); public abstract int line(); } }