Replace dom-3 with custom java code, and check xhtml references to contained content

This commit is contained in:
Grahame Grieve 2024-02-10 23:01:52 +11:00
parent 20bf70ed68
commit 3a1cc2e75a
2 changed files with 205 additions and 14 deletions

View File

@ -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<ValidationMessage> valerrors = new ArrayList<ValidationMessage>();
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<ValidationMessage> 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<ValidationMessage> 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<ValidationMessage> 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<ValidationMessage> 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<ValidationMessage> profileErrors = new ArrayList<ValidationMessage>();
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<ValidationMessage> 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<ValidationMessage> 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<ValidationMessage> errors, Element element, NodeStack stack) {
boolean ok = true;
Set<String> baseRefs = (Set<String>) element.getUserData(ValidationContext.INTERNAL_REFERENCES_NAME);
List<Element> 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<ValidationMessage> errors, NodeStack stack, boolean ok,
Set<String> baseRefs, List<Element> 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<String> baseRefs, List<Element> containedList, List<Element> ignoreList) {
String id = contained.getIdBase();
if (baseRefs.contains(id)) {
return true;
}
Set<String> irefs = (Set<String>) 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<String> refs = (Set<String>) c.getUserData(ValidationContext.INTERNAL_REFERENCES_NAME);
List<Element> ignoreList2 = new ArrayList<Element>();
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;

View File

@ -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<String, List<ValidationMessage>> sliceRecords;
private Set<String> 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<String> setupInternalRefs(Element element) {
Set<String> res = (Set<String>) element.getUserData(INTERNAL_REFERENCES_NAME);
if (res == null) {
res = new HashSet<String>();
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<String, List<ValidationMessage>>();
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<String, List<ValidationMessage>>();
res.dump("forSlicing");
return res;
@ -217,5 +240,9 @@ public class ValidationContext {
return this;
}
public Set<String> getInternalRefs() {
return internalRefs;
}
}