From 3a1cc2e75a14ce588c6c02cd1a76bfacc2fd35ed Mon Sep 17 00:00:00 2001 From: Grahame Grieve Date: Sat, 10 Feb 2024 23:01:52 +1100 Subject: [PATCH] Replace dom-3 with custom java code, and check xhtml references to contained content --- .../instance/InstanceValidator.java | 192 ++++++++++++++++-- .../instance/utils/ValidationContext.java | 27 +++ 2 files changed, 205 insertions(+), 14 deletions(-) diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/InstanceValidator.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/InstanceValidator.java index f6cafbbda..f33b96831 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/InstanceValidator.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/InstanceValidator.java @@ -494,18 +494,18 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat Element e = new ObjectConverter(context).convert((Resource) item); setParents(e); self.validateResource(new ValidationContext(ctxt.getAppContext(), e), valerrors, e, e, sd, IdStatus.OPTIONAL, new NodeStack(context, null, e, validationLanguage), null, - mode); + mode, false); } catch (IOException e1) { throw new FHIRException(e1); } } else if (item instanceof Element) { Element e = (Element) item; if (e.getSpecial() == SpecialElement.CONTAINED) { - self.validateResource(new ValidationContext(ctxt.getAppContext(), e, ctxt.getRootResource(), ctxt.getGroupingResource()), valerrors, e, e, sd, IdStatus.OPTIONAL, new NodeStack(context, null, e, validationLanguage), null, mode); + self.validateResource(new ValidationContext(ctxt.getAppContext(), e, ctxt.getRootResource(), ctxt.getGroupingResource()), valerrors, e, e, sd, IdStatus.OPTIONAL, new NodeStack(context, null, e, validationLanguage), null, mode, false); } else if (e.getSpecial() != null) { - self.validateResource(new ValidationContext(ctxt.getAppContext(), e, e, ctxt.getRootResource()), valerrors, e, e, sd, IdStatus.OPTIONAL, new NodeStack(context, null, e, validationLanguage), null, mode); + self.validateResource(new ValidationContext(ctxt.getAppContext(), e, e, ctxt.getRootResource()), valerrors, e, e, sd, IdStatus.OPTIONAL, new NodeStack(context, null, e, validationLanguage), null, mode, false); } else { - self.validateResource(new ValidationContext(ctxt.getAppContext(), e), valerrors, e, e, sd, IdStatus.OPTIONAL, new NodeStack(context, null, e, validationLanguage), null, mode); + self.validateResource(new ValidationContext(ctxt.getAppContext(), e), valerrors, e, e, sd, IdStatus.OPTIONAL, new NodeStack(context, null, e, validationLanguage), null, mode, false); } } else throw new NotImplementedException(context.formatMessage(I18nConstants.NOT_DONE_YET_VALIDATORHOSTSERVICESCONFORMSTOPROFILE_WHEN_ITEM_IS_NOT_AN_ELEMENT)); @@ -1002,7 +1002,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat long t = System.nanoTime(); NodeStack stack = new NodeStack(context, path, element, validationLanguage); if (profiles == null || profiles.isEmpty()) { - validateResource(new ValidationContext(appContext, element), errors, element, element, null, resourceIdRule, stack.resetIds(), null, new ValidationMode(ValidationReason.Validation, ProfileSource.BaseDefinition)); + validateResource(new ValidationContext(appContext, element), errors, element, element, null, resourceIdRule, stack.resetIds(), null, new ValidationMode(ValidationReason.Validation, ProfileSource.BaseDefinition), false); } else { int i = 0; while (i < profiles.size()) { @@ -1020,7 +1020,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat i++; } for (StructureDefinition defn : profiles) { - validateResource(new ValidationContext(appContext, element), errors, element, element, defn, resourceIdRule, stack.resetIds(), null, new ValidationMode(ValidationReason.Validation, ProfileSource.ConfigProfile)); + validateResource(new ValidationContext(appContext, element), errors, element, element, defn, resourceIdRule, stack.resetIds(), null, new ValidationMode(ValidationReason.Validation, ProfileSource.ConfigProfile), false); } } if (hintAboutNonMustSupport) { @@ -2319,10 +2319,10 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat if (!ok) { if (definition.hasUserData(XVerExtensionManager.XVER_EXT_MARKER)) { warning(errors, NO_RULE_DATE, IssueType.STRUCTURE, container.line(), container.col(), stack.getLiteralPath(), false, - modifier ? I18nConstants.EXTENSION_EXTM_CONTEXT_WRONG_XVER : I18nConstants.EXTENSION_EXTP_CONTEXT_WRONG_XVER, extUrl, contexts.toString(), plist.toString()); + modifier ? I18nConstants.EXTENSION_EXTM_CONTEXT_WRONG_XVER : I18nConstants.EXTENSION_EXTP_CONTEXT_WRONG_XVER, extUrl, contexts.toString(), plist.toString(), definition.getUserString(XVerExtensionManager.XVER_VER_MARKER)); } else { rule(errors, NO_RULE_DATE, IssueType.STRUCTURE, container.line(), container.col(), stack.getLiteralPath(), false, - modifier ? I18nConstants.EXTENSION_EXTM_CONTEXT_WRONG : I18nConstants.EXTENSION_EXTP_CONTEXT_WRONG, extUrl, contexts.toString(), plist.toString()); + modifier ? I18nConstants.EXTENSION_EXTM_CONTEXT_WRONG : I18nConstants.EXTENSION_EXTP_CONTEXT_WRONG, extUrl, contexts.toString(), plist.toString(), definition.getUserString(XVerExtensionManager.XVER_VER_MARKER)); } return false; } else { @@ -2399,7 +2399,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat } else if (sd.getType().equals(resource.fhirType())) { List valerrors = new ArrayList(); ValidationMode mode = new ValidationMode(ValidationReason.Expression, ProfileSource.FromExpression); - validateResource(new ValidationContext(appContext, resource), valerrors, resource, resource, sd, IdStatus.OPTIONAL, new NodeStack(context, null, resource, validationLanguage), null, mode); + validateResource(new ValidationContext(appContext, resource), valerrors, resource, resource, sd, IdStatus.OPTIONAL, new NodeStack(context, null, resource, validationLanguage), null, mode, false); boolean ok = true; List record = new ArrayList<>(); for (ValidationMessage v : valerrors) { @@ -2951,6 +2951,12 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat ok = checkInnerNames(errors, e, path, xhtml.getChildNodes(), false) && ok; ok = checkUrls(errors, e, path, xhtml.getChildNodes()) && ok; ok = checkIdRefs(errors, e, path, xhtml, resource) && ok; + if (true) { + ok = checkReferences(valContext, errors, e, path, "div", xhtml, resource) && ok; + } + if (true) { + ok = checkImageSources(valContext, errors, e, path, "div", xhtml, resource) && ok; + } } } @@ -3066,6 +3072,9 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat boolean ok = true; // now, do we check the URI target? if (fetcher != null && !type.equals("uuid")) { + if (url.startsWith("#")) { + valContext.getInternalRefs().add(url.substring(1)); + } boolean found; try { found = isDefinitionURL(url) || (allowExamples && (url.contains("example.org") || url.contains("acme.com")) || url.contains("acme.org")) /* || (url.startsWith("http://hl7.org/fhir/tools")) */ || @@ -3319,6 +3328,99 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat return ok; } + private boolean checkReferences(ValidationContext valContext, List errors, Element e, String path, String xpath, XhtmlNode node, Element resource) { + boolean ok = true; + if (node.getNodeType() == NodeType.Element & "a".equals(node.getName()) && node.getAttribute("href") != null) { + String href = node.getAttribute("href"); + if (!Utilities.noString(href) && href.startsWith("#") && !href.equals("#")) { + String ref = href.substring(1); + valContext.getInternalRefs().add(ref); + int count = countTargetMatches(resource, ref, true); + if (count == 0) { + rule(errors, NO_RULE_DATE, IssueType.INVALID, e.line(), e.col(), path, false, I18nConstants.TYPE_SPECIFIC_CHECKS_DT_XHTML_RESOLVE, href, xpath, node.allText()); + } else if (count > 1) { + warning(errors, NO_RULE_DATE, IssueType.INVALID, e.line(), e.col(), path, false, I18nConstants.TYPE_SPECIFIC_CHECKS_DT_XHTML_MULTIPLE_MATCHES, href, xpath, node.allText()); + } + } else { + // we can't validate at this point. Come back and revisit this some time in the future + } + } + if (node.hasChildren()) { + for (XhtmlNode child : node.getChildNodes()) { + checkReferences(valContext, errors, e, path, xpath+"/"+child.getName(), child, resource); + } + } + return ok; + } + + + protected int countTargetMatches(Element element, String fragment, boolean checkBundle) { + int count = 0; + if (fragment.equals(element.getIdBase())) { + count++; + } + if (element.getXhtml() != null) { + count = count + countTargetMatches(element.getXhtml(), fragment); + } + if (element.hasChildren()) { + for (Element child : element.getChildren()) { + count = count + countTargetMatches(child, fragment, false); + } + } + if (count == 0 && checkBundle) { + Element e = element.getParentForValidator(); + while (e != null) { + if (e.fhirType().equals("Bundle")) { + return countTargetMatches(e, fragment, false); + } + e = e.getParentForValidator(); + } + } + return count; + } + + private int countTargetMatches(XhtmlNode node, String fragment) { + int count = 0; + if (fragment.equals(node.getAttribute("id"))) { + count++; + } + if ("a".equals(node.getName()) && fragment.equals(node.getAttribute("name"))) { + count++; + } + if (node.hasChildren()) { + for (XhtmlNode child : node.getChildNodes()) { + count = count + countTargetMatches(child, fragment); + } + } + return count; + } + + + private boolean checkImageSources(ValidationContext valContext, List errors, Element e, String path, String xpath, XhtmlNode node, Element resource) { + boolean ok = true; + if (node.getNodeType() == NodeType.Element & "img".equals(node.getName()) && node.getAttribute("src") != null) { + String src = node.getAttribute("src"); + if (src.startsWith("#")) { + String ref = src.substring(1); + valContext.getInternalRefs().add(ref); + int count = countFragmentMatches(resource, ref); + if (count == 0) { + rule(errors, NO_RULE_DATE, IssueType.INVALID, e.line(), e.col(), path, false, I18nConstants.TYPE_SPECIFIC_CHECKS_DT_XHTML_RESOLVE_IMG, src, xpath); + } else if (count > 1) { + rule(errors, NO_RULE_DATE, IssueType.INVALID, e.line(), e.col(), path, false, I18nConstants.TYPE_SPECIFIC_CHECKS_DT_XHTML_MULTIPLE_MATCHES, src, xpath); + } + } else { + // we can't validate at this point. Come back and revisit this some time in the future + } + } + if (node.hasChildren()) { + for (XhtmlNode child : node.getChildNodes()) { + checkImageSources(valContext, errors, e, path, path+"/"+child.getName(), child, resource); + } + } + return ok; + } + private boolean checkIdRefs(List errors, Element e, String path, XhtmlNode node, Element resource) { boolean ok = true; if (node.getNodeType() == NodeType.Element && node.getAttribute("idref") != null) { @@ -3764,6 +3866,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat ok = bh.ok() && ok; String refType; if (ref.startsWith("#")) { + valContext.getInternalRefs().add(ref.substring(1)); refType = "contained"; } else { if (we == null) { @@ -3889,7 +3992,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat for (StructureDefinition pr : profiles) { List profileErrors = new ArrayList(); validateResource(we.valContext(valContext, pr), profileErrors, we.getResource(), we.getFocus(), pr, - IdStatus.OPTIONAL, we.getStack().resetIds(), pct, vmode.withReason(ValidationReason.MatchingSlice)); + IdStatus.OPTIONAL, we.getStack().resetIds(), pct, vmode.withReason(ValidationReason.MatchingSlice), true); if (!hasErrors(profileErrors)) { goodCount++; goodProfiles.put(pr, profileErrors); @@ -5829,7 +5932,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat if (rule(errors, NO_RULE_DATE, IssueType.INVALID, element.line(), element.col(), stack.getLiteralPath(), profile != null, I18nConstants.BUNDLE_BUNDLE_ENTRY_NOPROFILE_EXPL, special == null ? "??" : special.toHuman(), resourceName, typeForResource.getProfile().get(0).asStringValue())) { trackUsage(profile, valContext, element); - ok = validateResource(hc, errors, resource, element, profile, idstatus, stack, pct, mode) && ok; + ok = validateResource(hc, errors, resource, element, profile, idstatus, stack, pct, mode, false) && ok; } else { ok = false; } @@ -5841,7 +5944,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat trackUsage(profile, valContext, element); if (rule(errors, NO_RULE_DATE, IssueType.INVALID, element.line(), element.col(), stack.getLiteralPath(), profile != null, I18nConstants.BUNDLE_BUNDLE_ENTRY_NOPROFILE_TYPE, special == null ? "??" : special.toHuman(), resourceName)) { - ok = validateResource(hc, errors, resource, element, profile, idstatus, stack, pct, mode) && ok; + ok = validateResource(hc, errors, resource, element, profile, idstatus, stack, pct, mode, false) && ok; } else { ok = false; } @@ -5862,7 +5965,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat trackUsage(profile, valContext, element); List perrors = new ArrayList<>(); errorsList.add(perrors); - if (validateResource(hc, perrors, resource, element, profile, idstatus, stack, pct, mode)) { + if (validateResource(hc, perrors, resource, element, profile, idstatus, stack, pct, mode, false)) { bm.append(u.asStringValue()); matched++; } @@ -6942,6 +7045,11 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat if (debug) { System.out.println("inv "+inv.getKey()+" on "+path+" in "+resource.fhirType()+" {{ "+inv.getExpression()+" }}"+time()); } + // we don't allow dom-3 to execute - it takes too long (and is wrong). + // instead, we enforce it in code + if ("dom-3".equals(inv.getKey())) { + return true; + } ExpressionNode n = (ExpressionNode) inv.getUserData("validator.expression.cache"); if (n == null) { long t = System.nanoTime(); @@ -7014,7 +7122,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat * The actual base entry point for internal use (re-entrant) */ private boolean validateResource(ValidationContext valContext, List errors, Element resource, - Element element, StructureDefinition defn, IdStatus idstatus, NodeStack stack, PercentageTracker pct, ValidationMode mode) throws FHIRException { + Element element, StructureDefinition defn, IdStatus idstatus, NodeStack stack, PercentageTracker pct, ValidationMode mode, boolean forReference) throws FHIRException { boolean ok = true; // check here if we call validation policy here, and then change it to the new interface @@ -7070,6 +7178,10 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat } else { ok = false; } + if (!forReference) { + // last step: check that all contained resources are referenced or reference # + ok = checkContainedReferences(valContext, errors, element, stack) && ok; + } } if (testMode && ok == hasErrors(errors)) { throw new Error("ok is wrong. ok = "+ok+", errors = "+errorIds(stack.getLiteralPath(), ok, errors)); @@ -7077,6 +7189,58 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat return ok; } + private boolean checkContainedReferences(ValidationContext valContext, List errors, Element element, NodeStack stack) { + boolean ok = true; + Set baseRefs = (Set) element.getUserData(ValidationContext.INTERNAL_REFERENCES_NAME); + List containedList = element.getChildrenByName("contained"); + if (!containedList.isEmpty()) { + boolean allDone = true; + for (Element contained : containedList) { + allDone = allDone && contained.hasUserData(ValidationContext.INTERNAL_REFERENCES_NAME); + } + if (allDone) { + // We collected all the internal references in sets on the resource and the contained resources + int i = 0; + for (Element contained : containedList) { + ok = checkContainedReferences(errors, stack, ok, baseRefs, containedList, i, contained); + i++; + } + } + } + return ok; + } + + private boolean checkContainedReferences(List errors, NodeStack stack, boolean ok, + Set baseRefs, List containedList, int i, Element contained) { + NodeStack n = stack.push(contained, i, null, null); + boolean found = isReferencedFromBase(contained, baseRefs, containedList, new ArrayList<>()); + ok = rule(errors, NO_RULE_DATE, IssueType.INVALID, n, found, I18nConstants.CONTAINED_ORPHAN_DOM3, contained.getIdBase()) && ok; + return ok; + } + + private boolean isReferencedFromBase(Element contained, Set baseRefs, List containedList, List ignoreList) { + String id = contained.getIdBase(); + if (baseRefs.contains(id)) { + return true; + } + Set irefs = (Set) contained.getUserData(ValidationContext.INTERNAL_REFERENCES_NAME); + if (irefs.contains("")) { + return true; + } + for (Element c : containedList) { + if (c != contained && !ignoreList.contains(c)) { // ignore list is to prevent getting into an unterminated loop + Set refs = (Set) c.getUserData(ValidationContext.INTERNAL_REFERENCES_NAME); + List ignoreList2 = new ArrayList(); + ignoreList.addAll(ignoreList); + ignoreList.add(c); + if (refs != null && refs.contains(id) && isReferencedFromBase(c, baseRefs, containedList, ignoreList2)) { + return true; + } + } + } + return false; + } + private boolean checkResourceName(StructureDefinition defn, String resourceName, FhirFormat format) { if (resourceName.equals(defn.getType())) { return true; diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/utils/ValidationContext.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/utils/ValidationContext.java index b2acb49e8..c2eec6ada 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/utils/ValidationContext.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/utils/ValidationContext.java @@ -1,8 +1,10 @@ package org.hl7.fhir.validation.instance.utils; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import org.hl7.fhir.r5.elementmodel.Element; import org.hl7.fhir.r5.elementmodel.Manager.FhirFormat; @@ -11,6 +13,8 @@ import org.hl7.fhir.utilities.validation.ValidationMessage; public class ValidationContext { + public static final String INTERNAL_REFERENCES_NAME = "internal.references"; + private Object appContext; // the version we are currently validating for right now @@ -27,6 +31,7 @@ public class ValidationContext { private boolean checkSpecials = true; private Map> sliceRecords; + private Set internalRefs; public ValidationContext(Object appContext) { this.appContext = appContext; @@ -36,12 +41,22 @@ public class ValidationContext { this.appContext = appContext; this.resource = element; this.rootResource = element; + this.internalRefs = setupInternalRefs(element); check(); // no groupingResource (Bundle or Parameters) dump("creating"); } + private Set setupInternalRefs(Element element) { + Set res = (Set) element.getUserData(INTERNAL_REFERENCES_NAME); + if (res == null) { + res = new HashSet(); + element.setUserData(INTERNAL_REFERENCES_NAME, res); + } + return res; + } + private void check() { if (!rootResource.hasParentForValidator()) { throw new Error("No parent on root resource"); @@ -52,6 +67,7 @@ public class ValidationContext { this.appContext = appContext; this.resource = element; this.rootResource = root; + this.internalRefs = setupInternalRefs(element); check(); // no groupingResource (Bundle or Parameters) dump("creating"); @@ -62,6 +78,7 @@ public class ValidationContext { this.resource = element; this.rootResource = root; this.groupingResource = groupingResource; + this.internalRefs = setupInternalRefs(element); check(); dump("creating"); } @@ -137,6 +154,7 @@ public class ValidationContext { res.profile = profile; res.groupingResource = groupingResource; res.version = version; + res.internalRefs = setupInternalRefs(element); res.dump("forContained"); return res; } @@ -148,6 +166,7 @@ public class ValidationContext { res.profile = profile; res.groupingResource = groupingResource; res.version = version; + res.internalRefs = setupInternalRefs(element); res.dump("forEntry"); return res; } @@ -159,6 +178,7 @@ public class ValidationContext { res.profile = profile; res.version = version; res.groupingResource = groupingResource; + res.internalRefs = internalRefs; res.sliceRecords = sliceRecords != null ? sliceRecords : new HashMap>(); res.dump("forProfile "+profile.getUrl()); return res; @@ -171,6 +191,7 @@ public class ValidationContext { res.profile = profile; res.groupingResource = groupingResource; res.checkSpecials = false; + res.internalRefs = setupInternalRefs(resource); res.dump("forLocalReference "+profile.getUrl()); res.version = version; return res; @@ -191,6 +212,7 @@ public class ValidationContext { res.groupingResource = null; res.checkSpecials = false; res.version = version; + res.internalRefs = setupInternalRefs(resource); res.dump("forRemoteReference "+profile.getUrl()); return res; } @@ -203,6 +225,7 @@ public class ValidationContext { res.profile = profile; res.checkSpecials = false; res.version = version; + res.internalRefs = internalRefs; res.sliceRecords = new HashMap>(); res.dump("forSlicing"); return res; @@ -217,5 +240,9 @@ public class ValidationContext { return this; } + public Set getInternalRefs() { + return internalRefs; + } + } \ No newline at end of file