diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/context/BaseWorkerContext.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/context/BaseWorkerContext.java index 44be71cf6..7e073a55a 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/context/BaseWorkerContext.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/context/BaseWorkerContext.java @@ -111,6 +111,7 @@ import org.hl7.fhir.r5.terminologies.ValueSetExpander.TerminologyServiceErrorCla import org.hl7.fhir.r5.terminologies.ValueSetExpander.ValueSetExpansionOutcome; import org.hl7.fhir.r5.terminologies.ValueSetExpanderSimple; import org.hl7.fhir.r5.utils.ToolingExtensions; +import org.hl7.fhir.r5.utils.validation.ValidationContextCarrier; import org.hl7.fhir.utilities.OIDUtils; import org.hl7.fhir.utilities.TimeTracker; import org.hl7.fhir.utilities.ToolingClientLogger; @@ -927,6 +928,12 @@ public abstract class BaseWorkerContext extends I18nBase implements IWorkerConte @Override public ValidationResult validateCode(ValidationOptions options, Coding code, ValueSet vs) { + ValidationContextCarrier ctxt = new ValidationContextCarrier(); + return validateCode(options, code, vs, ctxt); + } + + @Override + public ValidationResult validateCode(ValidationOptions options, Coding code, ValueSet vs, ValidationContextCarrier ctxt) { if (options == null) { options = ValidationOptions.defaults(); } @@ -946,7 +953,7 @@ public abstract class BaseWorkerContext extends I18nBase implements IWorkerConte if (options.isUseClient()) { // ok, first we try to validate locally try { - ValueSetCheckerSimple vsc = new ValueSetCheckerSimple(options, vs, this); + ValueSetCheckerSimple vsc = new ValueSetCheckerSimple(options, vs, this, ctxt); if (!vsc.isServerSide(code.getSystem())) { res = vsc.validateCode(code); if (txCache != null) { @@ -1066,13 +1073,15 @@ public abstract class BaseWorkerContext extends I18nBase implements IWorkerConte } } if (vs != null) { - if (isTxCaching && cacheId != null && cached.contains(vs.getUrl()+"|"+vs.getVersion())) { + if (isTxCaching && cacheId != null && vs.getUrl() != null && cached.contains(vs.getUrl()+"|"+vs.getVersion())) { pin.addParameter().setName("url").setValue(new UriType(vs.getUrl()+(vs.hasVersion() ? "|"+vs.getVersion() : ""))); } else if (options.getVsAsUrl()){ pin.addParameter().setName("url").setValue(new StringType(vs.getUrl())); } else { pin.addParameter().setName("valueSet").setResource(vs); - cached.add(vs.getUrl()+"|"+vs.getVersion()); + if (vs.getUrl() != null) { + cached.add(vs.getUrl()+"|"+vs.getVersion()); + } } cache = true; addDependentResources(pin, vs); diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/context/IWorkerContext.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/context/IWorkerContext.java index d7ca6e3ac..ed0afccfc 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/context/IWorkerContext.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/context/IWorkerContext.java @@ -45,6 +45,7 @@ import org.hl7.fhir.exceptions.DefinitionException; import org.hl7.fhir.exceptions.FHIRException; import org.hl7.fhir.exceptions.TerminologyServiceException; import org.hl7.fhir.r5.context.TerminologyCache.CacheToken; +import org.hl7.fhir.r5.elementmodel.Element; import org.hl7.fhir.r5.formats.IParser; import org.hl7.fhir.r5.formats.ParserType; import org.hl7.fhir.r5.model.Bundle; @@ -64,11 +65,13 @@ import org.hl7.fhir.r5.model.ValueSet.ConceptSetComponent; import org.hl7.fhir.r5.terminologies.ValueSetExpander.TerminologyServiceErrorClass; import org.hl7.fhir.r5.terminologies.ValueSetExpander.ValueSetExpansionOutcome; import org.hl7.fhir.r5.utils.validation.IResourceValidator; +import org.hl7.fhir.r5.utils.validation.ValidationContextCarrier; import org.hl7.fhir.utilities.TimeTracker; import org.hl7.fhir.utilities.TranslationServices; import org.hl7.fhir.utilities.npm.BasePackageCacheManager; import org.hl7.fhir.utilities.npm.NpmPackage; import org.hl7.fhir.utilities.validation.ValidationMessage.IssueSeverity; +import org.hl7.fhir.utilities.validation.ValidationMessage; import org.hl7.fhir.utilities.validation.ValidationOptions; import com.google.gson.JsonSyntaxException; @@ -193,13 +196,14 @@ public interface IWorkerContext { } } + public interface ICanonicalResourceLocator { void findResource(Object caller, String url); // if it can be found, put it in the context } public interface IContextResourceLoader { /** - * @return List of the resource types that shoud be loaded + * @return List of the resource types that should be loaded */ String[] getTypes(); @@ -244,7 +248,6 @@ public interface IWorkerContext { IContextResourceLoader getNewLoader(NpmPackage npm) throws JsonSyntaxException, IOException; } - /** * Get the versions of the definitions loaded in context * @return @@ -745,6 +748,8 @@ public interface IWorkerContext { * @return */ public ValidationResult validateCode(ValidationOptions options, Coding code, ValueSet vs); + + public ValidationResult validateCode(ValidationOptions options, Coding code, ValueSet vs, ValidationContextCarrier ctxt); public void validateCodeBatch(ValidationOptions options, List codes, ValueSet vs); diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/terminologies/ValueSetCheckerSimple.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/terminologies/ValueSetCheckerSimple.java index 6a906bb49..706e70e7a 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/terminologies/ValueSetCheckerSimple.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/terminologies/ValueSetCheckerSimple.java @@ -56,6 +56,9 @@ import org.hl7.fhir.r5.model.ValueSet.ConceptSetComponent; import org.hl7.fhir.r5.model.ValueSet.ConceptSetFilterComponent; import org.hl7.fhir.r5.model.ValueSet.ValueSetExpansionContainsComponent; import org.hl7.fhir.r5.terminologies.ValueSetExpander.TerminologyServiceErrorClass; +import org.hl7.fhir.r5.utils.ToolingExtensions; +import org.hl7.fhir.r5.utils.validation.ValidationContextCarrier; +import org.hl7.fhir.r5.utils.validation.ValidationContextCarrier.ValidationContextResourceProxy; import org.hl7.fhir.utilities.CommaSeparatedStringBuilder; import org.hl7.fhir.utilities.Utilities; import org.hl7.fhir.utilities.i18n.I18nConstants; @@ -69,12 +72,52 @@ public class ValueSetCheckerSimple extends ValueSetWorker implements ValueSetChe private IWorkerContext context; private Map inner = new HashMap<>(); private ValidationOptions options; + private ValidationContextCarrier localContext; + private List localSystems = new ArrayList<>(); public ValueSetCheckerSimple(ValidationOptions options, ValueSet source, IWorkerContext context) { this.valueset = source; this.context = context; this.options = options; } + + public ValueSetCheckerSimple(ValidationOptions options, ValueSet source, IWorkerContext context, ValidationContextCarrier ctxt) { + this.valueset = source; + this.context = context; + this.options = options; + this.localContext = ctxt; + analyseValueSet(); + } + + private void analyseValueSet() { + if (localContext != null) { + if (valueset != null) { + for (ConceptSetComponent i : valueset.getCompose().getInclude()) { + analyseComponent(i); + } + for (ConceptSetComponent i : valueset.getCompose().getExclude()) { + analyseComponent(i); + } + } + } + } + + private void analyseComponent(ConceptSetComponent i) { + if (i.getSystemElement().hasExtension(ToolingExtensions.EXT_VALUESET_SYSTEM)) { + String ref = i.getSystemElement().getExtensionString(ToolingExtensions.EXT_VALUESET_SYSTEM); + if (ref.startsWith("#")) { + String id = ref.substring(1); + for (ValidationContextResourceProxy t : localContext.getResources()) { + CodeSystem cs = (CodeSystem) t.loadContainedResource(id, CodeSystem.class); + if (cs != null) { + localSystems.add(cs); + } + } + } else { + throw new Error("Not done yet #2: "+ref); + } + } + } public ValidationResult validateCode(CodeableConcept code) throws FHIRException { // first, we validate the codings themselves @@ -85,7 +128,7 @@ public class ValueSetCheckerSimple extends ValueSetWorker implements ValueSetChe if (!c.hasSystem()) { warnings.add(context.formatMessage(I18nConstants.CODING_HAS_NO_SYSTEM__CANNOT_VALIDATE)); } - CodeSystem cs = context.fetchCodeSystem(c.getSystem()); + CodeSystem cs = resolveCodeSystem(c.getSystem()); ValidationResult res = null; if (cs == null || cs.getContent() != CodeSystemContentMode.COMPLETE) { res = context.validateCode(options.noClient(), c, null); @@ -124,6 +167,19 @@ public class ValueSetCheckerSimple extends ValueSetWorker implements ValueSetChe } } + public CodeSystem resolveCodeSystem(String system) { + for (CodeSystem t : localSystems) { + if (t.getUrl().equals(system)) { + return t; + } + } + CodeSystem cs = context.fetchCodeSystem(system); + if (cs == null) { + cs = findSpecialCodeSystem(system); + } + return cs; + } + public ValidationResult validateCode(Coding code) throws FHIRException { String warningMessage = null; // first, we validate the concept itself @@ -144,10 +200,7 @@ public class ValueSetCheckerSimple extends ValueSetWorker implements ValueSetChe } inExpansion = checkExpansion(code); inInclude = checkInclude(code); - CodeSystem cs = context.fetchCodeSystem(system); - if (cs == null) { - cs = findSpecialCodeSystem(system); - } + CodeSystem cs = resolveCodeSystem(system); if (cs == null) { warningMessage = "Unable to resolve system "+system; if (!inExpansion) { @@ -498,7 +551,7 @@ public class ValueSetCheckerSimple extends ValueSetWorker implements ValueSetChe if (vsi.hasFilter()) { return null; } - CodeSystem cs = context.fetchCodeSystem(vsi.getSystem()); + CodeSystem cs = resolveCodeSystem(vsi.getSystem()); if (cs == null) { return null; } @@ -604,7 +657,7 @@ public class ValueSetCheckerSimple extends ValueSetWorker implements ValueSetChe if (!system.equals(vsi.getSystem())) return false; // ok, we need the code system - CodeSystem cs = context.fetchCodeSystem(system); + CodeSystem cs = resolveCodeSystem(system); if (cs == null || (cs.getContent() != CodeSystemContentMode.COMPLETE && cs.getContent() != CodeSystemContentMode.FRAGMENT)) { // make up a transient value set with ValueSet vs = new ValueSet(); @@ -709,7 +762,7 @@ public class ValueSetCheckerSimple extends ValueSetWorker implements ValueSetChe return inner.get(url); } ValueSet vs = context.fetchResource(ValueSet.class, url); - ValueSetCheckerSimple vsc = new ValueSetCheckerSimple(options, vs, context); + ValueSetCheckerSimple vsc = new ValueSetCheckerSimple(options, vs, context, localContext); inner.put(url, vsc); return vsc; } diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/validation/ValidationContextCarrier.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/validation/ValidationContextCarrier.java new file mode 100644 index 000000000..313919812 --- /dev/null +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/validation/ValidationContextCarrier.java @@ -0,0 +1,81 @@ +package org.hl7.fhir.r5.utils.validation; + +import java.util.ArrayList; +import java.util.List; + +import org.hl7.fhir.exceptions.FHIRException; +import org.hl7.fhir.r5.elementmodel.Element; +import org.hl7.fhir.r5.model.DomainResource; +import org.hl7.fhir.r5.model.Questionnaire; +import org.hl7.fhir.r5.model.Resource; +import org.hl7.fhir.utilities.validation.ValidationMessage; + +public class ValidationContextCarrier { + /** + * + * When the validator is calling validateCode, it typically has a partially loaded resource that may provide + * additional resources that are relevant to the validation. This is a handle back into the validator context + * to ask for the resource to be fully loaded if it becomes relevant. Note that the resource may fail to load + * (e.g. if it's part of what's being validated) and if it does, the validator will record the validation + * issues before throwing an error + * + * This is a reference back int + * + */ + public interface IValidationContextResourceLoader { + public Resource loadContainedResource(List errors, String path, Element resource, String id, Class class1) throws FHIRException; + } + + /** + * A list of resources that provide context - typically, a container resource, and a bundle resource. + * iterate these in order looking for contained resources + * + */ + public static class ValidationContextResourceProxy { + + // either a resource + private Resource resource; + + + // or an element and a loader + private Element element; + private IValidationContextResourceLoader loader; + private List errors; + private String path; + + public ValidationContextResourceProxy(Resource resource) { + this.resource = resource; + } + + public ValidationContextResourceProxy(List errors, String path, Element element, IValidationContextResourceLoader loader) { + this.errors = errors; + this.path = path; + this.element = element; + this.loader = loader; + } + + public Resource loadContainedResource(String id, Class class1) throws FHIRException { + if (resource == null) { + Resource res = loader.loadContainedResource(errors, path, element, id, class1); + return res; + } else { + if (resource instanceof DomainResource) { + for (Resource r : ((DomainResource) resource).getContained()) { + if (r.getId().equals(id)) { + if (class1.isInstance(r)) + return r; + } + } + } + return null; + } + } + } + + private List resources = new ArrayList<>(); + + public List getResources() { + return resources; + } + +} diff --git a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/i18n/I18nConstants.java b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/i18n/I18nConstants.java index 5eaf4c981..1a67ff30d 100644 --- a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/i18n/I18nConstants.java +++ b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/i18n/I18nConstants.java @@ -514,6 +514,7 @@ public class I18nConstants { public static final String TYPE_SPECIFIC_CHECKS_DT_URL_EXAMPLE = "TYPE_SPECIFIC_CHECKS_DT_URL_EXAMPLE"; public static final String TYPE_SPECIFIC_CHECKS_DT_CANONICAL_TYPE = "TYPE_SPECIFIC_CHECKS_DT_CANONICAL_TYPE"; public static final String TYPE_SPECIFIC_CHECKS_DT_CANONICAL_RESOLVE = "TYPE_SPECIFIC_CHECKS_DT_CANONICAL_RESOLVE"; + public static final String TYPE_SPECIFIC_CHECKS_DT_CANONICAL_RESOLVE_NC = "TYPE_SPECIFIC_CHECKS_DT_CANONICAL_RESOLVE_NC"; public static final String TYPE_SPECIFIC_CHECKS_DT_UUID_STRAT = "Type_Specific_Checks_DT_UUID_Strat"; public static final String TYPE_SPECIFIC_CHECKS_DT_UUID_VALID = "Type_Specific_Checks_DT_UUID_Valid"; public static final String UNABLE_TO_CONNECT_TO_TERMINOLOGY_SERVER = "Unable_to_connect_to_terminology_server"; diff --git a/org.hl7.fhir.utilities/src/main/resources/Messages.properties b/org.hl7.fhir.utilities/src/main/resources/Messages.properties index 7c435856d..bf567fc41 100644 --- a/org.hl7.fhir.utilities/src/main/resources/Messages.properties +++ b/org.hl7.fhir.utilities/src/main/resources/Messages.properties @@ -645,6 +645,7 @@ SD_ED_BIND_NO_BINDABLE = The element {0} has a binding, but no bindable types ar DISCRIMINATOR_BAD_PATH = Error processing path expression for discriminator: {0} (src = ''{1}'') SLICING_CANNOT_BE_EVALUATED = Slicing cannot be evaluated: {0} TYPE_SPECIFIC_CHECKS_DT_CANONICAL_RESOLVE = Canonical URL ''{0}'' does not resolve +TYPE_SPECIFIC_CHECKS_DT_CANONICAL_RESOLVE_NC = Canonical URL ''{0}'' exists, but can't be loaded, so it can't be checked for validity TYPE_SPECIFIC_CHECKS_DT_CANONICAL_TYPE = Canonical URL ''{0}'' refers to a resource that has the wrong type. Found {1} expecting one of {2} CODESYSTEM_CS_NO_SUPPLEMENT = CodeSystem {0} is a supplement, so can't be used as a value in Coding.system CODESYSTEM_CS_SUPP_CANT_CHECK = CodeSystem {0} cannot be found, so can't check if concepts are valid diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/BaseValidator.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/BaseValidator.java index fe10e899f..74c3102a1 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/BaseValidator.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/BaseValidator.java @@ -44,6 +44,7 @@ import org.hl7.fhir.r5.model.*; import org.hl7.fhir.r5.terminologies.ValueSetUtilities; import org.hl7.fhir.r5.utils.XVerExtensionManager; import org.hl7.fhir.r5.utils.XVerExtensionManager.XVerExtensionStatus; +import org.hl7.fhir.r5.utils.validation.ValidationContextCarrier.IValidationContextResourceLoader; import org.hl7.fhir.utilities.Utilities; import org.hl7.fhir.utilities.i18n.I18nConstants; import org.hl7.fhir.utilities.validation.ValidationMessage; @@ -61,7 +62,8 @@ import java.util.Map; import static org.apache.commons.lang3.StringUtils.isBlank; -public class BaseValidator { +public class BaseValidator implements IValidationContextResourceLoader { + public class TrackedLocationRelatedMessage { private Object location; private ValidationMessage vmsg; @@ -996,7 +998,8 @@ public class BaseValidator { } } - protected Resource loadContainedResource(List errors, String path, Element resource, String id, Class class1) throws FHIRException { + @Override + public Resource loadContainedResource(List errors, String path, Element resource, String id, Class class1) throws FHIRException { for (Element contained : resource.getChildren("contained")) { if (contained.getIdBase().equals(id)) { return loadFoundResource(errors, path, contained, class1); @@ -1005,7 +1008,7 @@ public class BaseValidator { return null; } - protected Resource loadFoundResource(List errors, String path, Element resource, Class class1) throws FHIRException { + protected Resource loadFoundResource(List errors, String path, Element resource, Class class1) throws FHIRException { try { FhirPublication v = FhirPublication.fromCode(context.getVersion()); ByteArrayOutputStream bs = new ByteArrayOutputStream(); diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/type/QuestionnaireValidator.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/type/QuestionnaireValidator.java index 73c70bda2..947f3ec59 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/type/QuestionnaireValidator.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/type/QuestionnaireValidator.java @@ -10,6 +10,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import org.attoparser.config.ParseConfiguration.ElementBalancing; import org.hl7.fhir.convertors.conv10_50.VersionConvertor_10_50; import org.hl7.fhir.convertors.conv14_50.VersionConvertor_14_50; import org.hl7.fhir.convertors.conv30_50.VersionConvertor_30_50; @@ -40,6 +41,8 @@ import org.hl7.fhir.r5.model.TimeType; import org.hl7.fhir.r5.model.ValueSet; import org.hl7.fhir.r5.utils.FHIRPathEngine; import org.hl7.fhir.r5.utils.XVerExtensionManager; +import org.hl7.fhir.r5.utils.validation.ValidationContextCarrier; +import org.hl7.fhir.r5.utils.validation.ValidationContextCarrier.ValidationContextResourceProxy; import org.hl7.fhir.utilities.CommaSeparatedStringBuilder; import org.hl7.fhir.utilities.Utilities; import org.hl7.fhir.utilities.i18n.I18nConstants; @@ -52,6 +55,7 @@ import org.hl7.fhir.validation.cli.utils.QuestionnaireMode; import org.hl7.fhir.validation.TimeTracker; import org.hl7.fhir.validation.instance.EnableWhenEvaluator; import org.hl7.fhir.validation.instance.EnableWhenEvaluator.QStack; +import org.hl7.fhir.validation.instance.type.QuestionnaireValidator.ElementWithIndex; import org.hl7.fhir.validation.instance.type.QuestionnaireValidator.QuestionnaireWithContext; import org.hl7.fhir.validation.instance.utils.NodeStack; import org.hl7.fhir.validation.instance.utils.ValidatorHostContext; @@ -60,6 +64,26 @@ import ca.uhn.fhir.util.ObjectUtil; public class QuestionnaireValidator extends BaseValidator { + public class ElementWithIndex { + + private Element element; + private int index; + + public ElementWithIndex(Element element, int index) { + this.element = element; + this.index = index; + } + + public Element getElement() { + return element; + } + + public int getIndex() { + return index; + } + + } + public static class QuestionnaireWithContext { private Questionnaire q; private Element container; @@ -88,7 +112,7 @@ public class QuestionnaireValidator extends BaseValidator { public Questionnaire q() { return q; } - + } private EnableWhenEvaluator myEnableWhenEvaluator; @@ -263,8 +287,9 @@ public class QuestionnaireValidator extends BaseValidator { if (answers.size() > 1) rule(errors, IssueType.INVALID, answers.get(1).line(), answers.get(1).col(), stack.getLiteralPath(), qItem.getRepeats(), I18nConstants.QUESTIONNAIRE_QR_ITEM_ONLYONEA); + int i = 0; for (Element answer : answers) { - NodeStack ns = stack.push(answer, -1, null, null); + NodeStack ns = stack.push(answer, i, null, null); if (qItem.getType() != null) { switch (qItem.getType()) { case GROUP: @@ -337,12 +362,15 @@ public class QuestionnaireValidator extends BaseValidator { case NULL: // no validation break; + case QUESTION: + throw new Error("Shouldn't get here?"); } } if (qItem.getType() != QuestionnaireItemType.GROUP) { // if it's a group, we already have an error before getting here, so no need to hammer away on that validateQuestionannaireResponseItems(hostContext, qsrc, qItem.getItem(), errors, answer, stack, inProgress, questionnaireResponseRoot, qstack); } + i++; } if (qItem.getType() == null) { fail(errors, IssueType.REQUIRED, element.line(), element.col(), stack.getLiteralPath(), false, I18nConstants.QUESTIONNAIRE_QR_ITEM_NOTYPE, qItem.getLinkId()); @@ -363,14 +391,13 @@ public class QuestionnaireValidator extends BaseValidator { return !answers.isEmpty() || !qItem.getRequired() || qItem.getType() == QuestionnaireItemType.GROUP; } - private void validateQuestionnaireResponseItem(ValidatorHostContext hostcontext, QuestionnaireWithContext qsrc, QuestionnaireItemComponent qItem, List errors, List elements, NodeStack stack, boolean inProgress, Element questionnaireResponseRoot, QStack qstack) { - if (elements.size() > 1) - rule(errors, IssueType.INVALID, elements.get(1).line(), elements.get(1).col(), stack.getLiteralPath(), qItem.getRepeats(), I18nConstants.QUESTIONNAIRE_QR_ITEM_ONLYONEI, qItem.getLinkId()); - int i = 0; - for (Element element : elements) { - NodeStack ns = stack.push(element, i, null, null); - validateQuestionnaireResponseItem(hostcontext, qsrc, qItem, errors, element, ns, inProgress, questionnaireResponseRoot, qstack.push(qItem, element)); - i++; + private void validateQuestionnaireResponseItem(ValidatorHostContext hostcontext, QuestionnaireWithContext qsrc, QuestionnaireItemComponent qItem, List errors, List elements, NodeStack stack, boolean inProgress, Element questionnaireResponseRoot, QStack qstack) { + if (elements.size() > 1) { + rule(errors, IssueType.INVALID, elements.get(1).getElement().line(), elements.get(1).getElement().col(), stack.getLiteralPath(), qItem.getRepeats(), I18nConstants.QUESTIONNAIRE_QR_ITEM_ONLYONEI, qItem.getLinkId()); + } + for (ElementWithIndex element : elements) { + NodeStack ns = stack.push(element.getElement(), element.getIndex(), null, null); + validateQuestionnaireResponseItem(hostcontext, qsrc, qItem, errors, element.getElement(), ns, inProgress, questionnaireResponseRoot, qstack.push(qItem, element.getElement())); } } @@ -386,8 +413,9 @@ public class QuestionnaireValidator extends BaseValidator { List items = new ArrayList(); element.getNamedChildren("item", items); // now, sort into stacks - Map> map = new HashMap>(); + Map> map = new HashMap>(); int lastIndex = -1; + int counter = 0; for (Element item : items) { String linkId = item.getNamedChildValue("linkId"); if (rule(errors, IssueType.REQUIRED, item.line(), item.col(), stack.getLiteralPath(), !Utilities.noString(linkId), I18nConstants.QUESTIONNAIRE_QR_ITEM_NOLINKID)) { @@ -396,7 +424,7 @@ public class QuestionnaireValidator extends BaseValidator { QuestionnaireItemComponent qItem = findQuestionnaireItem(qsrc, linkId); if (qItem != null) { rule(errors, IssueType.STRUCTURE, item.line(), item.col(), stack.getLiteralPath(), index > -1, misplacedItemError(qItem)); - NodeStack ns = stack.push(item, -1, null, null); + NodeStack ns = stack.push(item, counter, null, null); validateQuestionnaireResponseItem(hostContext, qsrc, qItem, errors, item, ns, inProgress, questionnaireResponseRoot, qstack.push(qItem, item)); } else rule(errors, IssueType.NOTFOUND, item.line(), item.col(), stack.getLiteralPath(), index > -1, I18nConstants.QUESTIONNAIRE_QR_ITEM_NOTFOUND, linkId); @@ -407,29 +435,28 @@ public class QuestionnaireValidator extends BaseValidator { // If an item has a child called "linkId" but no child called "answer", // we'll treat it as not existing for the purposes of enableWhen validation if (item.hasChildren("answer") || item.hasChildren("item")) { - List mapItem = map.computeIfAbsent(linkId, key -> new ArrayList<>()); - mapItem.add(item); + List mapItem = map.computeIfAbsent(linkId, key -> new ArrayList<>()); + mapItem.add(new ElementWithIndex(item, counter)); } } } + counter++; } // ok, now we have a list of known items, grouped by linkId. We've made an error for anything out of order for (QuestionnaireItemComponent qItem : qItems) { - List mapItem = map.get(qItem.getLinkId()); + List mapItem = map.get(qItem.getLinkId()); validateQuestionnaireResponseItem(hostContext, qsrc, errors, element, stack, inProgress, questionnaireResponseRoot, qItem, mapItem, qstack); } } - public void validateQuestionnaireResponseItem(ValidatorHostContext hostContext, QuestionnaireWithContext qsrc, List errors, Element element, NodeStack stack, boolean inProgress, Element questionnaireResponseRoot, QuestionnaireItemComponent qItem, List mapItem, QStack qstack) { + public void validateQuestionnaireResponseItem(ValidatorHostContext hostContext, QuestionnaireWithContext qsrc, List errors, Element element, NodeStack stack, boolean inProgress, Element questionnaireResponseRoot, QuestionnaireItemComponent qItem, List mapItem, QStack qstack) { boolean enabled = myEnableWhenEvaluator.isQuestionEnabled(hostContext, qItem, qstack, fpe); if (mapItem != null) { if (!enabled) { - int i = 0; - for (Element e : mapItem) { - NodeStack ns = stack.push(e, i, e.getProperty().getDefinition(), e.getProperty().getDefinition()); - rule(errors, IssueType.INVALID, e.line(), e.col(), ns.getLiteralPath(), enabled, I18nConstants.QUESTIONNAIRE_QR_ITEM_NOTENABLED2, qItem.getLinkId()); - i++; + for (ElementWithIndex e : mapItem) { + NodeStack ns = stack.push(e.getElement(), e.getElement().getIndex(), e.getElement().getProperty().getDefinition(), e.getElement().getProperty().getDefinition()); + rule(errors, IssueType.INVALID, e.getElement().line(), e.getElement().col(), ns.getLiteralPath(), enabled, I18nConstants.QUESTIONNAIRE_QR_ITEM_NOTENABLED2, qItem.getLinkId()); } } @@ -516,7 +543,8 @@ public class QuestionnaireValidator extends BaseValidator { } long t = System.nanoTime(); - ValidationResult res = context.validateCode(new ValidationOptions(stack.getWorkingLang()), c, vs); + ValidationContextCarrier vc = makeValidationContext(errors, qSrc); + ValidationResult res = context.validateCode(new ValidationOptions(stack.getWorkingLang()), c, vs, vc); timeTracker.tx(t, "vc "+c.getSystem()+"#"+c.getCode()+" '"+c.getDisplay()+"'"); if (!res.isOk()) { txRule(errors, res.getTxLink(), IssueType.CODEINVALID, value.line(), value.col(), stack.getLiteralPath(), false, I18nConstants.QUESTIONNAIRE_QR_ITEM_BADOPTION, c.getSystem(), c.getCode()); @@ -529,6 +557,16 @@ public class QuestionnaireValidator extends BaseValidator { } } + private ValidationContextCarrier makeValidationContext(List errors, QuestionnaireWithContext qSrc) { + ValidationContextCarrier vc = new ValidationContextCarrier(); + if (qSrc.container == null) { + vc.getResources().add(new ValidationContextResourceProxy(qSrc.q)); + } else { + vc.getResources().add(new ValidationContextResourceProxy(errors, qSrc.containerPath, qSrc.container, this)); + } + return vc; + } + private void validateAnswerCode(List errors, Element answer, NodeStack stack, QuestionnaireWithContext qSrc, QuestionnaireItemComponent qItem, boolean theOpenChoice) { Element v = answer.getNamedChild("valueCoding"); NodeStack ns = stack.push(v, -1, null, null);