diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index e69de29bb..28ab95b23 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -0,0 +1,11 @@ +Validator Changes: +* Mark it has an error if a JSON Array is empty +* Don't make wrong error reports for profiling resources in bundles +* fix bug checking unfixed values for HumanName patterns +* fix bug checking patterns (missed in some circumstances) +* fix bug checking type of resources in bundles +* improve messages around cardinality errors in profiles + +Other code changes: +* Render binding description in profile tables if it doesn't contain paragraphs +* fix bug with wrong value for contentReference in derived profiles (profiles do not and cannot change the value) 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 23c15cc22..36a9960a6 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 @@ -12,6 +12,7 @@ public class I18nConstants { public static final String ALL_OBSERVATIONS_SHOULD_HAVE_A_PERFORMER = "All_observations_should_have_a_performer"; public static final String ALL_OBSERVATIONS_SHOULD_HAVE_A_SUBJECT = "All_observations_should_have_a_subject"; public static final String ALL_OK = "ALL_OK"; + public static final String ARRAY_CANNOT_BE_EMPTY = "ARRAY_CANNOT_BE_EMPTY"; public static final String ATTEMPT_TO_A_SLICE_AN_ELEMENT_THAT_DOES_NOT_REPEAT__FROM__IN_ = "Attempt_to_a_slice_an_element_that_does_not_repeat__from__in_"; public static final String ATTEMPT_TO_REPLACE_ELEMENT_NAME_FOR_A_NONCHOICE_TYPE = "Attempt_to_replace_element_name_for_a_nonchoice_type"; public static final String ATTEMPT_TO_USE_A_SNAPSHOT_ON_PROFILE__AS__BEFORE_IT_IS_GENERATED = "Attempt_to_use_a_snapshot_on_profile__as__before_it_is_generated"; diff --git a/org.hl7.fhir.utilities/src/main/resources/Messages.properties b/org.hl7.fhir.utilities/src/main/resources/Messages.properties index 38e7af318..038b16b7c 100644 --- a/org.hl7.fhir.utilities/src/main/resources/Messages.properties +++ b/org.hl7.fhir.utilities/src/main/resources/Messages.properties @@ -209,11 +209,12 @@ Validation_BUNDLE_Message = The first entry in a message must be a MessageHeader Validation_VAL_Content_Unknown = Unrecognised Content {0} Validation_VAL_NoType = Unknown type {0} Validation_VAL_Profile_MatchMultiple = Profile {0}, Element matches more than one slice - {1}, {2} -Validation_VAL_Profile_Maximum = {0}: max allowed = {1}, but found {2} -Validation_VAL_Profile_Minimum = {0}: minimum required = {1}, but only found {2} +// for the next 4 messages, the available parameters are: 0: profile url, 1: ed.path, 2: ed.id, 3: ed.sliceName, 4: ed.label, 5: element.path, 6: ed.min and optionally 7: actual count +Validation_VAL_Profile_Maximum = {2}: max allowed = {6}, but found {7} (from {0}) +Validation_VAL_Profile_Minimum = {2}: minimum required = {6}, but only found {7} (from {0}) +Validation_VAL_Profile_NoCheckMax = {2}: Unable to check max allowed ({1}) due to lack of slicing validation (from {0}) +Validation_VAL_Profile_NoCheckMin = {2}: Unable to check minimum required ({1}) due to lack of slicing validation (from {0}) Validation_VAL_Profile_MultipleMatches = Found multiple matching profiles among choices: {0} -Validation_VAL_Profile_NoCheckMax = {0}: Unable to check max allowed ({1}) due to lack of slicing validation -Validation_VAL_Profile_NoCheckMin = {0}'': Unable to check minimum required ({1}) due to lack of slicing validation Validation_VAL_Profile_NoDefinition = No definition found for resource type ''{0}'' Validation_VAL_Profile_NoMatch = Unable to find matching profile among choices: {0} Validation_VAL_Profile_NoSnapshot = StructureDefinition has no snapshot - validation is against the snapshot, so it must be provided @@ -617,4 +618,5 @@ SD_ED_TYPE_PROFILE_NOTYPE = Found profile {0}, but unable to determine the type SD_ED_TYPE_PROFILE_WRONG = Profile {0} is for type {1}, but this element has type {2} TERMINOLOGY_TX_NOSVC_BOUND_REQ = Could not confirm that the codes provided are from the required value set {0} because there is no terminology service TERMINOLOGY_TX_NOSVC_BOUND_EXT = Could not confirm that the codes provided are from the extensible value set {0} because there is no terminology service +ARRAY_CANNOT_BE_EMPTY = Array cannot be empty - the property should not be present if it has no values \ No newline at end of file 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 0df8742e7..1727acab8 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 @@ -961,7 +961,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat } else { if (rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, codings.size() == fixed.getCoding().size(), I18nConstants.TERMINOLOGY_TX_CODING_COUNT, Integer.toString(fixed.getCoding().size()), Integer.toString(codings.size()))) { for (int i = 0; i < codings.size(); i++) - checkFixedValue(errors, path + ".coding", codings.get(i), fixed.getCoding().get(i), fixedSource, "coding", focus); + checkFixedValue(errors, path + ".coding", codings.get(i), fixed.getCoding().get(i), fixedSource, "coding", focus, false); } } } @@ -1748,10 +1748,6 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat return b.toString(); } - private void checkFixedValue(List errors, String path, Element focus, org.hl7.fhir.r5.model.Element fixed, String fixedSource, String propName, Element parent) { - checkFixedValue(errors, path, focus, fixed, fixedSource, propName, parent, false); - } - @SuppressWarnings("rawtypes") private void checkFixedValue(List errors, String path, Element focus, org.hl7.fhir.r5.model.Element fixed, String fixedSource, String propName, Element parent, boolean pattern) { if ((fixed == null || fixed.isEmpty()) && focus == null) { @@ -1829,7 +1825,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat for (Extension e : fixed.getExtension()) { Element ex = getExtensionByUrl(extensions, e.getUrl()); if (rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, ex != null, I18nConstants.EXTENSION_EXT_COUNT_NOTFOUND, e.getUrl())) { - checkFixedValue(errors, path, ex.getNamedChild("extension").getNamedChild("value"), e.getValue(), fixedSource, "extension.value", ex.getNamedChild("extension")); + checkFixedValue(errors, path, ex.getNamedChild("extension").getNamedChild("value"), e.getValue(), fixedSource, "extension.value", ex.getNamedChild("extension"), false); } } } @@ -1842,25 +1838,33 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat checkFixedValue(errors, path + ".period", focus.getNamedChild("period"), fixed.getPeriod(), fixedSource, "period", focus, pattern); List parts = new ArrayList(); - focus.getNamedChildren("family", parts); - if (rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, parts.size() > 0 == fixed.hasFamily(), I18nConstants.FIXED_TYPE_CHECKS_DT_NAME_FAMILY, (fixed.hasFamily() ? "1" : "0"), Integer.toString(parts.size()))) { - for (int i = 0; i < parts.size(); i++) - checkFixedValue(errors, path + ".family", parts.get(i), fixed.getFamilyElement(), fixedSource, "family", focus, pattern); + if (!pattern || fixed.hasFamily()) { + focus.getNamedChildren("family", parts); + if (rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, parts.size() > 0 == fixed.hasFamily(), I18nConstants.FIXED_TYPE_CHECKS_DT_NAME_FAMILY, (fixed.hasFamily() ? "1" : "0"), Integer.toString(parts.size()))) { + for (int i = 0; i < parts.size(); i++) + checkFixedValue(errors, path + ".family", parts.get(i), fixed.getFamilyElement(), fixedSource, "family", focus, pattern); + } } - focus.getNamedChildren("given", parts); - if (rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, parts.size() == fixed.getGiven().size(), I18nConstants.FIXED_TYPE_CHECKS_DT_NAME_GIVEN, Integer.toString(fixed.getGiven().size()), Integer.toString(parts.size()))) { - for (int i = 0; i < parts.size(); i++) - checkFixedValue(errors, path + ".given", parts.get(i), fixed.getGiven().get(i), fixedSource, "given", focus, pattern); + if (!pattern || fixed.hasGiven()) { + focus.getNamedChildren("given", parts); + if (rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, parts.size() == fixed.getGiven().size(), I18nConstants.FIXED_TYPE_CHECKS_DT_NAME_GIVEN, Integer.toString(fixed.getGiven().size()), Integer.toString(parts.size()))) { + for (int i = 0; i < parts.size(); i++) + checkFixedValue(errors, path + ".given", parts.get(i), fixed.getGiven().get(i), fixedSource, "given", focus, pattern); + } } - focus.getNamedChildren("prefix", parts); - if (rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, parts.size() == fixed.getPrefix().size(), I18nConstants.FIXED_TYPE_CHECKS_DT_NAME_PREFIX, Integer.toString(fixed.getPrefix().size()), Integer.toString(parts.size()))) { - for (int i = 0; i < parts.size(); i++) - checkFixedValue(errors, path + ".prefix", parts.get(i), fixed.getPrefix().get(i), fixedSource, "prefix", focus, pattern); + if (!pattern || fixed.hasPrefix()) { + focus.getNamedChildren("prefix", parts); + if (rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, parts.size() == fixed.getPrefix().size(), I18nConstants.FIXED_TYPE_CHECKS_DT_NAME_PREFIX, Integer.toString(fixed.getPrefix().size()), Integer.toString(parts.size()))) { + for (int i = 0; i < parts.size(); i++) + checkFixedValue(errors, path + ".prefix", parts.get(i), fixed.getPrefix().get(i), fixedSource, "prefix", focus, pattern); + } } - focus.getNamedChildren("suffix", parts); - if (rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, parts.size() == fixed.getSuffix().size(), I18nConstants.FIXED_TYPE_CHECKS_DT_NAME_SUFFIX, Integer.toString(fixed.getSuffix().size()), Integer.toString(parts.size()))) { - for (int i = 0; i < parts.size(); i++) - checkFixedValue(errors, path + ".suffix", parts.get(i), fixed.getSuffix().get(i), fixedSource, "suffix", focus, pattern); + if (!pattern || fixed.hasSuffix()) { + focus.getNamedChildren("suffix", parts); + if (rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, parts.size() == fixed.getSuffix().size(), I18nConstants.FIXED_TYPE_CHECKS_DT_NAME_SUFFIX, Integer.toString(fixed.getSuffix().size()), Integer.toString(parts.size()))) { + for (int i = 0; i < parts.size(); i++) + checkFixedValue(errors, path + ".suffix", parts.get(i), fixed.getSuffix().get(i), fixedSource, "suffix", focus, pattern); + } } } @@ -4082,8 +4086,12 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat // check type invariants checkInvariants(hostContext, errors, profile, definition, resource, element, stack, false); - if (definition.getFixed() != null) - checkFixedValue(errors, stack.getLiteralPath(), element, definition.getFixed(), profile.getUrl(), definition.getSliceName(), null); + if (definition.getFixed() != null) { + checkFixedValue(errors, stack.getLiteralPath(), element, definition.getFixed(), profile.getUrl(), definition.getSliceName(), null, false); + } + if (definition.getPattern() != null) { + checkFixedValue(errors, stack.getLiteralPath(), element, definition.getPattern(), profile.getUrl(), definition.getSliceName(), null, true); + } // get the list of direct defined children, including slices List childDefinitions = profileUtilities.getChildMap(profile, definition); @@ -4149,13 +4157,20 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat Element resource, Element element, String actualType, NodeStack stack, boolean inCodeableConcept, boolean checkDisplayInContext, ElementInfo ei, String extensionUrl) throws FHIRException, DefinitionException { + if (debug && ei.definition != null && ei.slice != null) { + System.out.println(Utilities.padLeft("", ' ', stack.depth())+ "Check "+ei.getPath()+" against both "+ei.definition.getId()+" and "+ei.slice.getId()); + } if (ei.definition != null) { - checkChildByDefinition(hostContext, errors, profile, definition, resource, element, actualType, stack, inCodeableConcept, checkDisplayInContext, ei, - extensionUrl, ei.definition, false); + if (debug) { + System.out.println(Utilities.padLeft("", ' ', stack.depth())+ "Check "+ei.getPath()+" against defn "+ei.definition.getId()); + } + checkChildByDefinition(hostContext, errors, profile, definition, resource, element, actualType, stack, inCodeableConcept, checkDisplayInContext, ei, extensionUrl, ei.definition, false); } if (ei.slice != null) { - checkChildByDefinition(hostContext, errors, profile, definition, resource, element, actualType, stack, inCodeableConcept, checkDisplayInContext, ei, - extensionUrl, ei.slice, true); + if (debug) { + System.out.println(Utilities.padLeft("", ' ', stack.depth())+ "Check "+ei.getPath()+" against slice "+ei.slice.getId()); + } + checkChildByDefinition(hostContext, errors, profile, definition, resource, element, actualType, stack, inCodeableConcept, checkDisplayInContext, ei, extensionUrl, ei.slice, true); } } @@ -4201,9 +4216,11 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat String prefix = tail(checkDefn.getPath()); assert typesAreAllReference(checkDefn.getType()) || checkDefn.hasRepresentation(PropertyRepresentation.TYPEATTR) || prefix.endsWith("[x]") || isResourceAndTypes(checkDefn) : "Multiple Types allowed, but name is wrong @ "+checkDefn.getPath()+": "+checkDefn.typeSummaryVB(); - if (checkDefn.hasRepresentation(PropertyRepresentation.TYPEATTR)) + if (checkDefn.hasRepresentation(PropertyRepresentation.TYPEATTR)) { type = ei.getElement().getType(); - else { + } else if (ei.getElement().isResource()) { + type = ei.getElement().fhirType(); + } else { prefix = prefix.substring(0, prefix.length() - 3); for (TypeRefComponent t : checkDefn.getType()) if ((prefix + Utilities.capitalize(t.getWorkingCode())).equals(ei.getName())) { @@ -4264,7 +4281,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat checkPrimitive(hostContext, errors, ei.getPath(), type, checkDefn, ei.getElement(), profile, stack); } else { if (checkDefn.hasFixed()) { - checkFixedValue(errors, ei.getPath(), ei.getElement(), checkDefn.getFixed(), profile.getUrl(), checkDefn.getSliceName(), null); + checkFixedValue(errors, ei.getPath(), ei.getElement(), checkDefn.getFixed(), profile.getUrl(), checkDefn.getSliceName(), null, false); } if (checkDefn.hasPattern()) { checkFixedValue(errors, ei.getPath(), ei.getElement(), checkDefn.getPattern(), profile.getUrl(), checkDefn.getSliceName(), null, true); @@ -4501,18 +4518,18 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat } } } - String location = "Profile " + profile.getUrl() + ", Element '" + stack.getLiteralPath() + "." + tail(ed.getPath()) + (ed.hasSliceName() ? "[" + ed.getSliceName() + (ed.hasLabel() ? " (" + ed.getLabel() + ")" : "") + "]" : "") + "'"; if (ed.getMin() > 0) { if (problematicPaths.contains(ed.getPath())) - hint(errors, IssueType.NOTSUPPORTED, element.line(), element.col(), stack.getLiteralPath(), count >= ed.getMin(), I18nConstants.VALIDATION_VAL_PROFILE_NOCHECKMIN, location, Integer.toString(ed.getMin())); + hint(errors, IssueType.NOTSUPPORTED, element.line(), element.col(), stack.getLiteralPath(), count >= ed.getMin(), I18nConstants.VALIDATION_VAL_PROFILE_NOCHECKMIN, profile.getUrl(), ed.getPath(), ed.getId(), ed.getSliceName(),ed.getLabel(), stack.getLiteralPath(), Integer.toString(ed.getMin())); else - rule(errors, IssueType.STRUCTURE, element.line(), element.col(), stack.getLiteralPath(), count >= ed.getMin(), I18nConstants.VALIDATION_VAL_PROFILE_MINIMUM, location, Integer.toString(ed.getMin()), Integer.toString(count)); + rule(errors, IssueType.STRUCTURE, element.line(), element.col(), stack.getLiteralPath(), count >= ed.getMin(), I18nConstants.VALIDATION_VAL_PROFILE_MINIMUM, profile.getUrl(), ed.getPath(), ed.getId(), ed.getSliceName(),ed.getLabel(), stack.getLiteralPath(), Integer.toString(ed.getMin()), Integer.toString(count)); } if (ed.hasMax() && !ed.getMax().equals("*")) { if (problematicPaths.contains(ed.getPath())) - hint(errors, IssueType.NOTSUPPORTED, element.line(), element.col(), stack.getLiteralPath(), count <= Integer.parseInt(ed.getMax()), I18nConstants.VALIDATION_VAL_PROFILE_NOCHECKMAX, location, ed.getMax()); - else - rule(errors, IssueType.STRUCTURE, element.line(), element.col(), stack.getLiteralPath(), count <= Integer.parseInt(ed.getMax()), I18nConstants.VALIDATION_VAL_PROFILE_MAXIMUM, location, ed.getMax(), Integer.toString(count)); + hint(errors, IssueType.NOTSUPPORTED, element.line(), element.col(), stack.getLiteralPath(), count <= Integer.parseInt(ed.getMax()), I18nConstants.VALIDATION_VAL_PROFILE_NOCHECKMAX, profile.getUrl(), ed.getPath(), ed.getId(), ed.getSliceName(),ed.getLabel(), stack.getLiteralPath(), ed.getMax()); + else if (count > Integer.parseInt(ed.getMax())) { + rule(errors, IssueType.STRUCTURE, element.line(), element.col(), stack.getLiteralPath(), false, I18nConstants.VALIDATION_VAL_PROFILE_MAXIMUM, profile.getUrl(), ed.getPath(), ed.getId(), ed.getSliceName(),ed.getLabel(), stack.getLiteralPath(), ed.getMax(), Integer.toString(count)); + } } } } @@ -4888,7 +4905,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat private boolean valueMatchesCriteria(Element value, ElementDefinition criteria, StructureDefinition profile) throws FHIRException { if (criteria.hasFixed()) { List msgs = new ArrayList(); - checkFixedValue(msgs, "{virtual}", value, criteria.getFixed(), profile.getUrl(), "value", null); + checkFixedValue(msgs, "{virtual}", value, criteria.getFixed(), profile.getUrl(), "value", null, false); return msgs.size() == 0; } else if (criteria.hasBinding() && criteria.getBinding().getStrength() == BindingStrength.REQUIRED && criteria.getBinding().hasValueSet()) { throw new FHIRException(context.formatMessage(I18nConstants.UNABLE_TO_RESOLVE_SLICE_MATCHING__SLICE_MATCHING_BY_VALUE_SET_NOT_DONE));