From ec72b1fcddef52cb3eb5d3527533478fba7491f2 Mon Sep 17 00:00:00 2001 From: Grahame Grieve Date: Sun, 5 Mar 2023 18:29:11 +1100 Subject: [PATCH] fix parsing and validating of concept maps in structure maps + validate terminology part of structure maps --- .../hl7/fhir/r5/elementmodel/FmlParser.java | 23 ++- .../r5/terminologies/CodeSystemUtilities.java | 14 ++ .../r5/terminologies/ConceptMapUtilities.java | 36 ++++ .../r5/terminologies/ValueSetUtilities.java | 17 ++ .../structuremap/StructureMapUtilities.java | 3 + .../fhir/utilities/i18n/I18nConstants.java | 25 +++ .../src/main/resources/Messages.properties | 28 ++- .../instance/InstanceValidator.java | 3 + .../instance/type/ConceptMapValidator.java | 162 ++++++++++++++++++ .../instance/type/StructureMapValidator.java | 103 +++++++++++ 10 files changed, 404 insertions(+), 10 deletions(-) create mode 100644 org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/type/ConceptMapValidator.java diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/elementmodel/FmlParser.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/elementmodel/FmlParser.java index be7c86c70..1b9779542 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/elementmodel/FmlParser.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/elementmodel/FmlParser.java @@ -12,8 +12,10 @@ import org.hl7.fhir.exceptions.DefinitionException; import org.hl7.fhir.exceptions.FHIRException; import org.hl7.fhir.exceptions.FHIRFormatError; import org.hl7.fhir.r5.context.IWorkerContext; +import org.hl7.fhir.r5.elementmodel.Element.SpecialElement; import org.hl7.fhir.r5.formats.IParser.OutputStyle; import org.hl7.fhir.r5.model.ExpressionNode; +import org.hl7.fhir.r5.model.StructureDefinition; import org.hl7.fhir.r5.model.ConceptMap.ConceptMapGroupUnmappedMode; import org.hl7.fhir.r5.model.Enumerations.ConceptMapRelationship; import org.hl7.fhir.r5.model.Enumerations.PublicationStatus; @@ -78,6 +80,10 @@ public class FmlParser extends ParserBase { } } lexer.setMetadataFormat(false); + if (!result.hasChild("status")) { + result.makeElement("status").setValue("draft"); + } + while (lexer.hasToken("conceptmap")) parseConceptMap(result, lexer); @@ -86,6 +92,9 @@ public class FmlParser extends ParserBase { while (lexer.hasToken("imports")) parseImports(result, lexer); + while (lexer.hasToken("conceptmap")) + parseConceptMap(result, lexer); + while (!lexer.done()) { parseGroup(result, lexer); } @@ -94,24 +103,22 @@ public class FmlParser extends ParserBase { } catch (Exception e) { logError("2023-02-24", -1, -1, "?", IssueType.INVALID, e.getMessage(), IssueSeverity.FATAL); } - - if (!result.hasChild("status")) { - result.makeElement("status").setValue("draft"); - } result.setIgnorePropertyOrder(true); return result; } - private void parseConceptMap(Element result, FHIRLexer lexer) throws FHIRLexerException { + private void parseConceptMap(Element structureMap, FHIRLexer lexer) throws FHIRLexerException { lexer.token("conceptmap"); - Element map = Manager.build(context, context.fetchTypeDefinition("ConceptMap")); + Element map = structureMap.makeElement("contained"); + StructureDefinition sd = context.fetchTypeDefinition("ConceptMap"); + map.updateProperty(new Property(context, sd.getSnapshot().getElement().get(0), sd), SpecialElement.fromProperty(map.getProperty()), map.getProperty()); + map.setType("ConceptMap"); Element eid = map.makeElement("id").markLocation(lexer.getCurrentLocation()); String id = lexer.readConstant("map id"); if (id.startsWith("#")) throw lexer.error("Concept Map identifier must start with #"); eid.setValue(id); - map.makeElement("status").setValue(PublicationStatus.DRAFT.toCode()); // todo: how to add this to the text format - result.makeElement("contained").setElement("resource", map); + map.makeElement("status").setValue(structureMap.getChildValue("status")); lexer.token("{"); // lexer.token("source"); // map.setSource(new UriType(lexer.readConstant("source"))); diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/terminologies/CodeSystemUtilities.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/terminologies/CodeSystemUtilities.java index d130424b0..c050e29fc 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/terminologies/CodeSystemUtilities.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/terminologies/CodeSystemUtilities.java @@ -47,6 +47,7 @@ import org.hl7.fhir.r5.model.CanonicalResource; import org.hl7.fhir.r5.model.CanonicalType; import org.hl7.fhir.r5.model.CodeSystem; import org.hl7.fhir.r5.model.CodeSystem.ConceptDefinitionComponent; +import org.hl7.fhir.r5.model.CodeSystem.ConceptDefinitionDesignationComponent; import org.hl7.fhir.r5.model.CodeSystem.ConceptPropertyComponent; import org.hl7.fhir.r5.model.CodeSystem.PropertyComponent; import org.hl7.fhir.r5.model.CodeSystem.PropertyType; @@ -678,5 +679,18 @@ public class CodeSystemUtilities { public static boolean isNotCurrent(CodeSystem cs, ConceptDefinitionComponent c) { return isInactive(cs, c) || isDeprecated(cs, c, false); } + + public static List getDisplays(CodeSystem srcCS, ConceptDefinitionComponent cd) { + List list = new ArrayList<>(); + if (cd.hasDisplay()) { + list.add(cd.getDisplay()); + } + for (ConceptDefinitionDesignationComponent d : cd.getDesignation()) { + if (!list.contains(d.getValue())) { + list.add(d.getValue()); + } + } + return list; + } } diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/terminologies/ConceptMapUtilities.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/terminologies/ConceptMapUtilities.java index 3878c0fda..07e033883 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/terminologies/ConceptMapUtilities.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/terminologies/ConceptMapUtilities.java @@ -1,6 +1,13 @@ package org.hl7.fhir.r5.terminologies; +import java.util.ArrayList; +import java.util.List; + +import org.hl7.fhir.r5.model.Coding; import org.hl7.fhir.r5.model.ConceptMap; +import org.hl7.fhir.r5.model.ConceptMap.ConceptMapGroupComponent; +import org.hl7.fhir.r5.model.ConceptMap.SourceElementComponent; +import org.hl7.fhir.r5.model.ConceptMap.TargetElementComponent; import org.hl7.fhir.r5.model.Identifier; import org.hl7.fhir.r5.model.ValueSet; @@ -30,5 +37,34 @@ public class ConceptMapUtilities { cm.addIdentifier().setSystem("urn:ietf:rfc:3986").setValue(oid); } + public static boolean hasMappingForSource(ConceptMap cm, String system, String version, String code) { + for (ConceptMapGroupComponent grp : cm.getGroup()) { + if (system.equals(grp.getSource())) { // to do: version + for (SourceElementComponent e : grp.getElement()) { + if (code.equals(e.getCode())) { + return true; // doesn't matter if it's actually unmapped + } + } + } + } + return false; + } + + public static List listTargets(ConceptMap cm, List systems) { + List list = new ArrayList<>(); + for (ConceptMapGroupComponent grp : cm.getGroup()) { + if (systems.isEmpty() || systems.contains(grp.getSource())) { // to do: version + for (SourceElementComponent e : grp.getElement()) { + for (TargetElementComponent t : e.getTarget()) { + if (t.hasCode()) { + list.add(new Coding(grp.getTarget(), t.getCode(), t.getDisplay())); + } + } + } + } + } + return list; + } + } diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/terminologies/ValueSetUtilities.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/terminologies/ValueSetUtilities.java index 407950cd0..4ae05f157 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/terminologies/ValueSetUtilities.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/terminologies/ValueSetUtilities.java @@ -51,6 +51,7 @@ import org.hl7.fhir.r5.model.ValueSet; import org.hl7.fhir.r5.model.CodeSystem.ConceptDefinitionComponent; import org.hl7.fhir.r5.model.CodeSystem.ConceptPropertyComponent; import org.hl7.fhir.r5.model.CodeType; +import org.hl7.fhir.r5.model.Coding; import org.hl7.fhir.r5.model.ValueSet.ConceptReferenceComponent; import org.hl7.fhir.r5.model.ValueSet.ConceptSetComponent; import org.hl7.fhir.r5.model.ValueSet.ValueSetComposeComponent; @@ -311,5 +312,21 @@ public class ValueSetUtilities { } } + public static boolean hasCodeInExpansion(ValueSet vs, Coding code) { + return hasCodeInExpansion(vs.getExpansion().getContains(), code); + } + + private static boolean hasCodeInExpansion(List list, Coding code) { + for (ValueSetExpansionContainsComponent c : list) { + if (c.getSystem().equals(code.getSystem()) && c.getCode().equals(code.getCode())) { + return true; + } + if (hasCodeInExpansion(c.getContains(), code)) { + return true; + } + } + return false; + } + } \ No newline at end of file diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/structuremap/StructureMapUtilities.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/structuremap/StructureMapUtilities.java index 205db2d64..63f5a3d6c 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/structuremap/StructureMapUtilities.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/structuremap/StructureMapUtilities.java @@ -660,6 +660,9 @@ public class StructureMapUtilities { while (lexer.hasToken("imports")) parseImports(result, lexer); + while (lexer.hasToken("conceptmap")) + parseConceptMap(result, lexer); + while (!lexer.done()) { parseGroup(result, lexer); } 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 887ef1da2..9305b0eeb 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 @@ -659,6 +659,10 @@ public class I18nConstants { public static final String CODESYSTEM_SHAREABLE_MISSING_HL7 = "CODESYSTEM_SHAREABLE_MISSING_HL7"; public static final String CODESYSTEM_SHAREABLE_EXTRA_MISSING_HL7 = "CODESYSTEM_SHAREABLE_EXTRA_MISSING_HL7"; public static final String CODESYSTEM_SHAREABLE_EXTRA_MISSING = "CODESYSTEM_SHAREABLE_EXTRA_MISSING"; + public static final String CONCEPTMAP_SHAREABLE_MISSING = "CONCEPTMAP_SHAREABLE_MISSING"; + public static final String CONCEPTMAP_SHAREABLE_MISSING_HL7 = "CONCEPTMAP_SHAREABLE_MISSING_HL7"; + public static final String CONCEPTMAP_SHAREABLE_EXTRA_MISSING_HL7 = "CONCEPTMAP_SHAREABLE_EXTRA_MISSING_HL7"; + public static final String CONCEPTMAP_SHAREABLE_EXTRA_MISSING = "CONCEPTMAP_SHAREABLE_EXTRA_MISSING"; public static final String MEASURE_SHAREABLE_MISSING = "MEASURE_SHAREABLE_MISSING"; public static final String MEASURE_SHAREABLE_MISSING_HL7 = "MEASURE_SHAREABLE_MISSING_HL7"; public static final String MEASURE_SHAREABLE_EXTRA_MISSING_HL7 = "MEASURE_SHAREABLE_EXTRA_MISSING_HL7"; @@ -814,6 +818,27 @@ public class I18nConstants { public static final String SM_SOURCE_TYPE_NOT_FOUND = "SM_SOURCE_TYPE_NOT_FOUND"; public static final String SM_TARGET_TYPE_NOT_FOUND = "SM_TARGET_TYPE_NOT_FOUND"; public static final String SM_MATCHING_RULEGROUP_NOT_FOUND = "SM_MATCHING_RULEGROUP_NOT_FOUND"; + public static final String SM_TARGET_TRANSFORM_MISSING_PARAMS = "SM_TARGET_TRANSFORM_MISSING_PARAMS"; + public static final String SM_TARGET_TRANSFORM_TRANSLATE_NO_PARAM = "SM_TARGET_TRANSFORM_TRANSLATE_NO_PARAM"; + public static final String SM_TARGET_TRANSFORM_TRANSLATE_UNKNOWN_SOURCE = "SM_TARGET_TRANSFORM_TRANSLATE_UNKNOWN_SOURCE"; + public static final String SM_TARGET_TRANSFORM_TRANSLATE_CM_NOT_FOUND = "SM_TARGET_TRANSFORM_TRANSLATE_CM_NOT_FOUND"; + public static final String SM_TARGET_TRANSFORM_TRANSLATE_CM_BAD_MODE = "SM_TARGET_TRANSFORM_TRANSLATE_CM_BAD_MODE"; + public static final String SM_TARGET_TRANSLATE_BINDING_SOURCE = "SM_TARGET_TRANSLATE_BINDING_SOURCE"; + public static final String SM_TARGET_TRANSLATE_BINDING_VS_SOURCE = "SM_TARGET_TRANSLATE_BINDING_VS_SOURCE"; + public static final String SM_TARGET_TRANSLATE_BINDING_VSE_SOURCE = "SM_TARGET_TRANSLATE_BINDING_VSE_SOURCE"; + public static final String SM_TARGET_TRANSLATE_BINDING_SOURCE_UNMAPPED = "SM_TARGET_TRANSLATE_BINDING_SOURCE_UNMAPPED"; + public static final String SM_TARGET_TRANSLATE_BINDING_TARGET = "SM_TARGET_TRANSLATE_BINDING_TARGET"; + public static final String SM_TARGET_TRANSLATE_BINDING_VS_TARGET = "SM_TARGET_TRANSLATE_BINDING_VS_TARGET"; + public static final String SM_TARGET_TRANSLATE_BINDING_VSE_TARGET = "SM_TARGET_TRANSLATE_BINDING_VSE_TARGET"; + public static final String SM_TARGET_TRANSLATE_BINDING_TARGET_WRONG = "SM_TARGET_TRANSLATE_BINDING_TARGET_WRONG"; + public static final String CONCEPTMAP_GROUP_SOURCE_MISSING = "CONCEPTMAP_GROUP_SOURCE_MISSING"; + public static final String CONCEPTMAP_GROUP_SOURCE_UNKNOWN = "CONCEPTMAP_GROUP_SOURCE_UNKNOWN"; + public static final String CONCEPTMAP_GROUP_TARGET_MISSING = "CONCEPTMAP_GROUP_TARGET_MISSING"; + public static final String CONCEPTMAP_GROUP_TARGET_UNKNOWN = "CONCEPTMAP_GROUP_TARGET_UNKNOWN"; + public static final String CONCEPTMAP_GROUP_SOURCE_CODE_INVALID = "CONCEPTMAP_GROUP_SOURCE_CODE_INVALID"; + public static final String CONCEPTMAP_GROUP_SOURCE_DISPLAY_INVALID = "CONCEPTMAP_GROUP_SOURCE_DISPLAY_INVALID"; + public static final String CONCEPTMAP_GROUP_TARGET_CODE_INVALID = "CONCEPTMAP_GROUP_TARGET_CODE_INVALID"; + public static final String CONCEPTMAP_GROUP_TARGET_DISPLAY_INVALID = "CONCEPTMAP_GROUP_TARGET_DISPLAY_INVALID"; } diff --git a/org.hl7.fhir.utilities/src/main/resources/Messages.properties b/org.hl7.fhir.utilities/src/main/resources/Messages.properties index 8164767e2..bf8351467 100644 --- a/org.hl7.fhir.utilities/src/main/resources/Messages.properties +++ b/org.hl7.fhir.utilities/src/main/resources/Messages.properties @@ -778,6 +778,10 @@ CODESYSTEM_SHAREABLE_MISSING = The ShareableCodeSystem profile says that the {0} CODESYSTEM_SHAREABLE_EXTRA_MISSING = The ShareableCodeSystem profile recommends that the {0} element is populated, but it is not present. Published code systems SHOULD conform to the ShareableCodeSystem profile CODESYSTEM_SHAREABLE_MISSING_HL7 = The ShareableCodeSystem profile says that the {0} element is mandatory, but it is not found. HL7 Published code systems SHALL conform to the ShareableCodeSystem profile CODESYSTEM_SHAREABLE_EXTRA_MISSING_HL7 = The ShareableCodeSystem profile recommends that the {0} element is populated, but it is not found. HL7 Published code systems SHALL conform to the ShareableCodeSystem profile +CONCEPTMAP_SHAREABLE_MISSING = The ShareableConceptMap profile says that the {0} element is mandatory, but it is not present. Published concept maps SHOULD conform to the ShareableConceptMap profile +CONCEPTMAP_SHAREABLE_EXTRA_MISSING = The ShareableConceptMap profile recommends that the {0} element is populated, but it is not present. Published concept maps SHOULD conform to the ShareableConceptMap profile +CONCEPTMAP_SHAREABLE_MISSING_HL7 = The ShareableConceptMap profile says that the {0} element is mandatory, but it is not found. HL7 Published concept maps SHALL conform to the ShareableConceptMap profile +CONCEPTMAP_SHAREABLE_EXTRA_MISSING_HL7 = The ShareableConceptMap profile recommends that the {0} element is populated, but it is not found. HL7 Published concept maps SHALL conform to the ShareableConceptMap profile MEASURE_SHAREABLE_MISSING = The ShareableMeasure profile says that the {0} element is mandatory, but it is not present. Published measures SHOULD conform to the ShareableMeasure profile MEASURE_SHAREABLE_EXTRA_MISSING = The ShareableMeasure profile recommends that the {0} element is populated, but it is not present. Published measures SHOULD conform to the ShareableMeasure profile MEASURE_SHAREABLE_MISSING_HL7 = The ShareableMeasure profile says that the {0} element is mandatory, but it is not found. HL7 Published measures SHALL conform to the ShareableMeasure profile @@ -851,7 +855,7 @@ SM_TARGET_PATH_MULTIPLE_MATCHES = The target path {0}.{1} refers to the path {2} SM_SOURCE_TYPE_INVALID = The type {0} is not valid in this source context {1}. The possible types are [{2}] SM_TARGET_TRANSFORM_PARAM_COUNT_RANGE = Transform {0} takes {1}-{2} parameter(s) but {3} were found SM_TARGET_TRANSFORM_PARAM_COUNT_SINGLE = Transform {0} takes {1} parameter(s) but {2} were found -SM_TARGET_TRANSFORM_NOT_CHECKED = Transform {0} not checked yet +SM_TARGET_TRANSFORM_NOT_CHECKED = Transform {0} not checked dyet SM_TARGET_NO_TRANSFORM_NO_CHECKED = When there is no transform, parameters can''t be provided SM_TARGET_TRANSFORM_TYPE_UNPROCESSIBLE = The value of the type parameter could not be processed SM_TARGET_TRANSFORM_PARAM_UNPROCESSIBLE = The parameter at index {0} could not be processed (type = {1}) @@ -864,7 +868,27 @@ SM_ORPHAN_GROUP = This group is not called from within this mapping script, and SM_SOURCE_TYPE_NOT_FOUND = No source type was found, so the default group for this implied dependent rule could not be determined SM_TARGET_TYPE_NOT_FOUND = No target type was found, so the default group for this implied dependent rule could not be determined SM_MATCHING_RULEGROUP_NOT_FOUND = Unable to find a default rule for the type pair source={0} and target={1} - +SM_TARGET_TRANSFORM_MISSING_PARAMS = One or more parameters to the translate operation are missing (should be 3, was {0}) +SM_TARGET_TRANSFORM_TRANSLATE_NO_PARAM = No value for the {0} parameter found +SM_TARGET_TRANSFORM_TRANSLATE_UNKNOWN_SOURCE = The source variable {0} is unknown +SM_TARGET_TRANSFORM_TRANSLATE_CM_NOT_FOUND = The map_uri ''{0}'' could not be resolved, so the map can't be checked +SM_TARGET_TRANSFORM_TRANSLATE_CM_BAD_MODE = The value ''{0}'' for the output parameter is incorrect +SM_TARGET_TRANSLATE_BINDING_SOURCE = The source variable does not have a required binding, so this concept map can''t be checked +SM_TARGET_TRANSLATE_BINDING_VS_SOURCE = The source variable refers to an unknown value set ''{0}'', so this concept map can''t be checked +SM_TARGET_TRANSLATE_BINDING_VSE_SOURCE = There was an error expanding the source value set, so this concept map can''t be checked: ''{0}'' +SM_TARGET_TRANSLATE_BINDING_SOURCE_UNMAPPED = The source value set includes one or more codes that the map does not translate: {0} +SM_TARGET_TRANSLATE_BINDING_TARGET = The target variable does not have a required binding, so this concept map can''t be checked +SM_TARGET_TRANSLATE_BINDING_VS_TARGET = The target variable refers to an unknown value set ''{0}'', so this concept map can''t be checked +SM_TARGET_TRANSLATE_BINDING_VSE_TARGET = There was an error expanding the target value set, so this concept map can''t be checked: ''{0}'' +SM_TARGET_TRANSLATE_BINDING_TARGET_WRONG = The map produces one or more codes that the target value set does not include: {0} +CONCEPTMAP_GROUP_SOURCE_MISSING = No Source Code System, so the source codes cannot be checked +CONCEPTMAP_GROUP_SOURCE_UNKNOWN = Unknown Source Code System, so the source codes cannot be checked +CONCEPTMAP_GROUP_TARGET_MISSING = No Target Code System, so the source codes cannot be checked +CONCEPTMAP_GROUP_TARGET_UNKNOWN = Unknown Target Code System, so the source codes cannot be checked +CONCEPTMAP_GROUP_SOURCE_CODE_INVALID = The source code ''{0}'' is not valid in the code system {1} +CONCEPTMAP_GROUP_SOURCE_DISPLAY_INVALID = The source display ''{0}'' is not valid. Possible codes {1} +CONCEPTMAP_GROUP_TARGET_CODE_INVALID =The target code ''{0}'' is not valid in the code system {1} +CONCEPTMAP_GROUP_TARGET_DISPLAY_INVALID = The target display ''{0}'' is not valid. Possible codes {1} \ 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 2dc161448..6b2a8b92b 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 @@ -185,6 +185,7 @@ import org.hl7.fhir.validation.cli.model.HtmlInMarkdownCheck; import org.hl7.fhir.validation.cli.utils.QuestionnaireMode; import org.hl7.fhir.validation.instance.type.BundleValidator; import org.hl7.fhir.validation.instance.type.CodeSystemValidator; +import org.hl7.fhir.validation.instance.type.ConceptMapValidator; import org.hl7.fhir.validation.instance.type.MeasureValidator; import org.hl7.fhir.validation.instance.type.QuestionnaireValidator; import org.hl7.fhir.validation.instance.type.SearchParameterValidator; @@ -5036,6 +5037,8 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat return validateCapabilityStatement(errors, element, stack); } else if (element.getType().equals("CodeSystem")) { return new CodeSystemValidator(context, timeTracker, this, xverManager, jurisdiction).validateCodeSystem(errors, element, stack, baseOptions.setLanguage(stack.getWorkingLang())); + } else if (element.getType().equals("ConceptMap")) { + return new ConceptMapValidator(context, timeTracker, this, xverManager, jurisdiction).validateConceptMap(errors, element, stack, baseOptions.setLanguage(stack.getWorkingLang())); } else if (element.getType().equals("SearchParameter")) { return new SearchParameterValidator(context, timeTracker, fpe, xverManager, jurisdiction).validateSearchParameter(errors, element, stack); } else if (element.getType().equals("StructureDefinition")) { diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/type/ConceptMapValidator.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/type/ConceptMapValidator.java new file mode 100644 index 000000000..ad9a06331 --- /dev/null +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/type/ConceptMapValidator.java @@ -0,0 +1,162 @@ +package org.hl7.fhir.validation.instance.type; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.hl7.fhir.exceptions.FHIRException; +import org.hl7.fhir.r5.context.IWorkerContext; +import org.hl7.fhir.r5.elementmodel.Element; +import org.hl7.fhir.r5.model.CodeSystem; +import org.hl7.fhir.r5.model.CodeSystem.ConceptDefinitionComponent; +import org.hl7.fhir.r5.model.Coding; +import org.hl7.fhir.r5.model.ValueSet; +import org.hl7.fhir.r5.terminologies.CodeSystemUtilities; +import org.hl7.fhir.r5.utils.XVerExtensionManager; +import org.hl7.fhir.utilities.Utilities; +import org.hl7.fhir.utilities.i18n.I18nConstants; +import org.hl7.fhir.utilities.validation.ValidationMessage; +import org.hl7.fhir.utilities.validation.ValidationMessage.IssueType; +import org.hl7.fhir.utilities.validation.ValidationMessage.Source; +import org.hl7.fhir.utilities.validation.ValidationOptions; +import org.hl7.fhir.validation.BaseValidator; +import org.hl7.fhir.validation.TimeTracker; +import org.hl7.fhir.validation.instance.InstanceValidator; +import org.hl7.fhir.validation.instance.utils.NodeStack; + +public class ConceptMapValidator extends BaseValidator { + + private InstanceValidator parent; + + public ConceptMapValidator(IWorkerContext context, TimeTracker timeTracker, InstanceValidator parent, XVerExtensionManager xverManager, Coding jurisdiction) { + super(context, xverManager); + source = Source.InstanceValidator; + this.timeTracker = timeTracker; + this.jurisdiction = jurisdiction; + this.parent = parent; + } + + public boolean validateConceptMap(List errors, Element cm, NodeStack stack, ValidationOptions options) { + boolean ok = true; + Map props = new HashMap<>(); + Map attribs = new HashMap<>(); + List groups = cm.getChildrenByName("group"); + int ci = 0; + for (Element group : groups) { + ok = validateGroup(errors, group, stack.push(group, ci, null, null), props, attribs) && ok; + ci++; + } + + if (!stack.isContained()) { + ok = checkShareableConceptMap(errors, cm, stack) && ok; + } + return ok; + } + + + private boolean validateGroup(List errors, Element grp, NodeStack stack, Map props, Map attribs) { + boolean ok = true; + CodeSystem srcCS = null; + CodeSystem tgtCS = null; + Element e = grp.getNamedChild("source"); + if (warning(errors, "2023-03-05", IssueType.REQUIRED, grp.line(), grp.col(), stack.getLiteralPath(), e != null, I18nConstants.CONCEPTMAP_GROUP_SOURCE_MISSING)) { + srcCS = context.fetchCodeSystem(e.getValue()); + warning(errors, "2023-03-05", IssueType.NOTFOUND, grp.line(), grp.col(), stack.push(e, -1, null, null).getLiteralPath(), srcCS != null, I18nConstants.CONCEPTMAP_GROUP_SOURCE_UNKNOWN, e.getValue()); + } + e = grp.getNamedChild("target"); + if (warning(errors, "2023-03-05", IssueType.REQUIRED, grp.line(), grp.col(), stack.getLiteralPath(), e != null, I18nConstants.CONCEPTMAP_GROUP_TARGET_MISSING)) { + tgtCS = context.fetchCodeSystem(e.getValue()); + warning(errors, "2023-03-05", IssueType.NOTFOUND, grp.line(), grp.col(), stack.push(e, -1, null, null).getLiteralPath(), tgtCS != null, I18nConstants.CONCEPTMAP_GROUP_TARGET_UNKNOWN, e.getValue()); + } + List elements = grp.getChildrenByName("element"); + int ci = 0; + for (Element element : elements) { + ok = validateGroupElement(errors, element, stack.push(element, ci, null, null), srcCS, tgtCS, props, attribs) && ok; + ci++; + } + return ok; + } + + private boolean validateGroupElement(List errors, Element src, NodeStack stack, CodeSystem srcCS, CodeSystem tgtCS, Map props, Map attribs) { + boolean ok = true; + + Element code = src.getNamedChild("code"); + if (code != null && srcCS != null) { + String c = code.getValue(); + ConceptDefinitionComponent cd = CodeSystemUtilities.getCode(srcCS, c); + if (rule(errors, "2023-03-05", IssueType.REQUIRED, code.line(), code.col(), stack.push(code, -1, null, null).getLiteralPath(), cd != null, I18nConstants.CONCEPTMAP_GROUP_SOURCE_CODE_INVALID, c, srcCS.getVersionedUrl())) { + Element display = src.getNamedChild("display"); + if (display != null) { + List displays = CodeSystemUtilities.getDisplays(srcCS, cd); + ok = rule(errors, "2023-03-05", IssueType.REQUIRED, code.line(), code.col(), stack.push(code, -1, null, null).getLiteralPath(), displays.contains(display.getValue()), I18nConstants.CONCEPTMAP_GROUP_SOURCE_DISPLAY_INVALID, display.getValue(), displays) && ok; + } + } else { + ok = false; + } + } + + List targets = src.getChildrenByName("target"); + int ci = 0; + for (Element target : targets) { + ok = validateGroupTargetElement(errors, target, stack.push(target, ci, null, null), srcCS, tgtCS, props, attribs) && ok; + ci++; + } + return ok; + } + + private boolean validateGroupTargetElement(List errors, Element tgt, NodeStack stack, CodeSystem srcCS, CodeSystem tgtCS, Map props, Map attribs) { + boolean ok = true; + + Element code = tgt.getNamedChild("code"); + if (code != null && tgtCS != null) { + String c = code.getValue(); + ConceptDefinitionComponent cd = CodeSystemUtilities.getCode(tgtCS, c); + if (rule(errors, "2023-03-05", IssueType.REQUIRED, code.line(), code.col(), stack.push(code, -1, null, null).getLiteralPath(), cd != null, I18nConstants.CONCEPTMAP_GROUP_TARGET_CODE_INVALID, c, tgtCS.getVersionedUrl())) { + Element display = tgt.getNamedChild("display"); + if (display != null) { + List displays = CodeSystemUtilities.getDisplays(tgtCS, cd); + ok = rule(errors, "2023-03-05", IssueType.REQUIRED, code.line(), code.col(), stack.push(code, -1, null, null).getLiteralPath(), displays.contains(display.getValue()), I18nConstants.CONCEPTMAP_GROUP_TARGET_DISPLAY_INVALID, display.getValue(), displays) && ok; + } + } else { + ok = false; + } + } + + return ok; + } + + private boolean checkShareableConceptMap(List errors, Element cs, NodeStack stack) { + if (parent.isForPublication()) { + if (isHL7(cs)) { + boolean ok = true; + ok = rule(errors, NO_RULE_DATE, IssueType.REQUIRED, cs.line(), cs.col(), stack.getLiteralPath(), cs.hasChild("url"), I18nConstants.CONCEPTMAP_SHAREABLE_MISSING_HL7, "url") && ok; + ok = rule(errors, NO_RULE_DATE, IssueType.REQUIRED, cs.line(), cs.col(), stack.getLiteralPath(), cs.hasChild("version"), I18nConstants.CONCEPTMAP_SHAREABLE_MISSING_HL7, "version") && ok; + ok = rule(errors, NO_RULE_DATE, IssueType.REQUIRED, cs.line(), cs.col(), stack.getLiteralPath(), cs.hasChild("title"), I18nConstants.CONCEPTMAP_SHAREABLE_MISSING_HL7, "title") && ok; + warning(errors, NO_RULE_DATE, IssueType.REQUIRED, cs.line(), cs.col(), stack.getLiteralPath(), cs.hasChild("name"), I18nConstants.CONCEPTMAP_SHAREABLE_EXTRA_MISSING_HL7, "name"); + ok = rule(errors, NO_RULE_DATE, IssueType.REQUIRED, cs.line(), cs.col(), stack.getLiteralPath(), cs.hasChild("status"), I18nConstants.CONCEPTMAP_SHAREABLE_MISSING_HL7, "status") && ok; + ok = rule(errors, NO_RULE_DATE, IssueType.REQUIRED, cs.line(), cs.col(), stack.getLiteralPath(), cs.hasChild("experimental"), I18nConstants.CONCEPTMAP_SHAREABLE_MISSING_HL7, "experimental") && ok; + ok = rule(errors, NO_RULE_DATE, IssueType.REQUIRED, cs.line(), cs.col(), stack.getLiteralPath(), cs.hasChild("description"), I18nConstants.CONCEPTMAP_SHAREABLE_MISSING_HL7, "description") && ok; + ok = rule(errors, NO_RULE_DATE, IssueType.REQUIRED, cs.line(), cs.col(), stack.getLiteralPath(), cs.hasChild("content"), I18nConstants.CONCEPTMAP_SHAREABLE_MISSING_HL7, "content") && ok; + if (!"supplement".equals(cs.getChildValue("content"))) { + ok = rule(errors, NO_RULE_DATE, IssueType.REQUIRED, cs.line(), cs.col(), stack.getLiteralPath(), cs.hasChild("caseSensitive"), I18nConstants.CONCEPTMAP_SHAREABLE_MISSING_HL7, "caseSensitive") && ok; + } + return ok; + } else { + warning(errors, NO_RULE_DATE, IssueType.REQUIRED, cs.line(), cs.col(), stack.getLiteralPath(), cs.hasChild("url"), I18nConstants.CONCEPTMAP_SHAREABLE_MISSING, "url"); + warning(errors, NO_RULE_DATE, IssueType.REQUIRED, cs.line(), cs.col(), stack.getLiteralPath(), cs.hasChild("version"), I18nConstants.CONCEPTMAP_SHAREABLE_MISSING, "version"); + warning(errors, NO_RULE_DATE, IssueType.REQUIRED, cs.line(), cs.col(), stack.getLiteralPath(), cs.hasChild("title"), I18nConstants.CONCEPTMAP_SHAREABLE_MISSING, "title"); + warning(errors, NO_RULE_DATE, IssueType.REQUIRED, cs.line(), cs.col(), stack.getLiteralPath(), cs.hasChild("name"), I18nConstants.CONCEPTMAP_SHAREABLE_EXTRA_MISSING, "name"); + warning(errors, NO_RULE_DATE, IssueType.REQUIRED, cs.line(), cs.col(), stack.getLiteralPath(), cs.hasChild("status"), I18nConstants.CONCEPTMAP_SHAREABLE_MISSING, "status"); + warning(errors, NO_RULE_DATE, IssueType.REQUIRED, cs.line(), cs.col(), stack.getLiteralPath(), cs.hasChild("experimental"), I18nConstants.CONCEPTMAP_SHAREABLE_MISSING, "experimental"); + warning(errors, NO_RULE_DATE, IssueType.REQUIRED, cs.line(), cs.col(), stack.getLiteralPath(), cs.hasChild("description"), I18nConstants.CONCEPTMAP_SHAREABLE_MISSING, "description"); + warning(errors, NO_RULE_DATE, IssueType.REQUIRED, cs.line(), cs.col(), stack.getLiteralPath(), cs.hasChild("content"), I18nConstants.CONCEPTMAP_SHAREABLE_MISSING, "content"); + if (!"supplement".equals(cs.getChildValue("content"))) { + warning(errors, NO_RULE_DATE, IssueType.REQUIRED, cs.line(), cs.col(), stack.getLiteralPath(), cs.hasChild("caseSensitive"), I18nConstants.CONCEPTMAP_SHAREABLE_MISSING, "caseSensitive"); + } + } + } + return true; + } + + +} \ No newline at end of file diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/type/StructureMapValidator.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/type/StructureMapValidator.java index 21350e352..343b797e1 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/type/StructureMapValidator.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/type/StructureMapValidator.java @@ -4,13 +4,18 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import org.hl7.fhir.exceptions.FHIRException; import org.hl7.fhir.r5.conformance.profile.ProfileUtilities; import org.hl7.fhir.r5.context.ContextUtilities; import org.hl7.fhir.r5.context.IWorkerContext; import org.hl7.fhir.r5.elementmodel.Element; import org.hl7.fhir.r5.model.Coding; +import org.hl7.fhir.r5.model.ConceptMap; import org.hl7.fhir.r5.model.ElementDefinition; import org.hl7.fhir.r5.model.ElementDefinition.TypeRefComponent; +import org.hl7.fhir.r5.model.Enumerations.BindingStrength; +import org.hl7.fhir.r5.model.Property; +import org.hl7.fhir.r5.model.Resource; import org.hl7.fhir.r5.model.StructureDefinition; import org.hl7.fhir.r5.model.StructureMap; import org.hl7.fhir.r5.model.StructureMap.StructureMapGroupComponent; @@ -20,6 +25,12 @@ import org.hl7.fhir.r5.model.StructureMap.StructureMapInputMode; import org.hl7.fhir.r5.model.StructureMap.StructureMapModelMode; import org.hl7.fhir.r5.model.StructureMap.StructureMapStructureComponent; import org.hl7.fhir.r5.model.TypeDetails; +import org.hl7.fhir.r5.model.ValueSet; +import org.hl7.fhir.r5.model.ValueSet.ConceptSetComponent; +import org.hl7.fhir.r5.model.ValueSet.ValueSetExpansionContainsComponent; +import org.hl7.fhir.r5.terminologies.ConceptMapUtilities; +import org.hl7.fhir.r5.terminologies.ValueSetExpander.ValueSetExpansionOutcome; +import org.hl7.fhir.r5.terminologies.ValueSetUtilities; import org.hl7.fhir.r5.utils.FHIRPathEngine; import org.hl7.fhir.r5.utils.XVerExtensionManager; import org.hl7.fhir.r5.utils.structuremap.StructureMapUtilities; @@ -668,6 +679,46 @@ public class StructureMapValidator extends BaseValidator { ok = false; } break; + case "translate": + ok = rule(errors, "2023-03-01", IssueType.INVALID, target.line(), target.col(), stack.getLiteralPath(), params.size() == 3, I18nConstants.SM_TARGET_TRANSFORM_MISSING_PARAMS, transform) && ok; + Element srcE = params.size() > 0 ? params.get(0).getNamedChild("value") : null; + Element mapE = params.size() > 1? params.get(1).getNamedChild("value") : null; + Element modeE = params.size() > 2 ? params.get(2).getNamedChild("value") : null; + VariableDefn sv = null; + // srcE - if it's an id, the variable must exist + if (rule(errors, "2023-03-01", IssueType.INVALID, target.line(), target.col(), stack.getLiteralPath(), srcE != null, I18nConstants.SM_TARGET_TRANSFORM_TRANSLATE_NO_PARAM, "source")) { + if ("id".equals(srcE.fhirType())) { + sv = variables.getVariable(srcE.getValue(), true); + rule(errors, "2023-03-01", IssueType.INVALID, target.line(), target.col(), stack.getLiteralPath(), sv != null, I18nConstants.SM_TARGET_TRANSFORM_TRANSLATE_UNKNOWN_SOURCE, srcE.getValue()); + } + } else { + ok = false; + } + // mapE - it must resolve (may be reference to contained) + if (rule(errors, "2023-03-01", IssueType.INVALID, target.line(), target.col(), stack.getLiteralPath(), mapE != null, I18nConstants.SM_TARGET_TRANSFORM_TRANSLATE_NO_PARAM, "map_uri")) { + String ref = mapE.getValue(); + ConceptMap cm = null; + if (ref.startsWith("#")) { + cm = (ConceptMap) loadContainedResource(errors, stack.getLiteralPath(), src, ref.substring(1), ConceptMap.class); + ok = rule(errors, "2023-03-01", IssueType.NOTFOUND, target.line(), target.col(), stack.getLiteralPath(), srcE != null, I18nConstants.SM_TARGET_TRANSFORM_TRANSLATE_CM_NOT_FOUND, ref) && ok; + } else { + // todo: look in Bundle? + cm = this.context.fetchResource(ConceptMap.class, ref); + warning(errors, "2023-03-01", IssueType.NOTFOUND, target.line(), target.col(), stack.getLiteralPath(), srcE != null, I18nConstants.SM_TARGET_TRANSFORM_TRANSLATE_CM_NOT_FOUND, ref); + } + if (cm != null && (v != null && v.hasTypeInfo() || (sv != null && sv.hasTypeInfo()))) { + ok = checkConceptMap(errors, target.line(), target.col(), stack.getLiteralPath(), cm, sv == null ? null : sv.getEd(), el == null ? null : el.getEd()) && ok; + } + } + if (modeE != null) { + String t = modeE.getValue(); + if (rule(errors, "2023-03-01", IssueType.INVALID, target.line(), target.col(), stack.getLiteralPath(), Utilities.existsInList(t, "code", "system", "display", "Coding", "CodeableConcept"), I18nConstants.SM_TARGET_TRANSFORM_TRANSLATE_CM_BAD_MODE, t)) { + // cross check the type + } else { + ok = false; + } + } + break; default: rule(errors, "2023-03-01", IssueType.INVALID, target.line(), target.col(), stack.getLiteralPath(), false, I18nConstants.SM_TARGET_TRANSFORM_NOT_CHECKED, transform); ok = false; @@ -688,6 +739,58 @@ public class StructureMapValidator extends BaseValidator { } } return ok; + + } + + private boolean checkConceptMap(List errors, int line, int col, String literalPath, ConceptMap cm, ElementDefinition srcED, ElementDefinition tgtED) { + boolean ok = true; + ValueSet srcVS = null; + if (srcED != null) { + if (warning(errors, "2023-03-01", IssueType.INVALID, line, col, literalPath, srcED.getBinding().hasValueSet() && srcED.getBinding().getStrength() == BindingStrength.REQUIRED, I18nConstants.SM_TARGET_TRANSLATE_BINDING_SOURCE)) { + srcVS = context.fetchResource(ValueSet.class, srcED.getBinding().getValueSet()); + if (warning(errors, "2023-03-01", IssueType.INVALID, line, col, literalPath, srcVS != null, I18nConstants.SM_TARGET_TRANSLATE_BINDING_VS_SOURCE)) { + ValueSetExpansionOutcome vse = context.expandVS(srcVS, true, false); + if (warning(errors, "2023-03-01", IssueType.INVALID, line, col, literalPath, vse.isOk(), I18nConstants.SM_TARGET_TRANSLATE_BINDING_VSE_SOURCE, vse.getError())) { + CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder(); + for (ValueSetExpansionContainsComponent c : vse.getValueset().getExpansion().getContains()) { + if (ConceptMapUtilities.hasMappingForSource(cm, c.getSystem(), c.getVersion(), c.getCode())) { + b.append(c.getCode()); + } + } + if (b.count() > 0) { + warning(errors, "2023-03-01", IssueType.INVALID, line, col, literalPath, srcED.getBinding().hasValueSet() && srcED.getBinding().getStrength() == BindingStrength.REQUIRED, I18nConstants.SM_TARGET_TRANSLATE_BINDING_SOURCE_UNMAPPED, b.toString()); + } + } + } + } + } + if (srcED != null) { + if (warning(errors, "2023-03-01", IssueType.INVALID, line, col, literalPath, tgtED.getBinding().hasValueSet() && tgtED.getBinding().getStrength() == BindingStrength.REQUIRED, I18nConstants.SM_TARGET_TRANSLATE_BINDING_TARGET)) { + ValueSet vs = context.fetchResource(ValueSet.class, tgtED.getBinding().getValueSet()); + if (warning(errors, "2023-03-01", IssueType.INVALID, line, col, literalPath, vs != null, I18nConstants.SM_TARGET_TRANSLATE_BINDING_VS_TARGET)) { + ValueSetExpansionOutcome vse = context.expandVS(vs, true, false); + if (warning(errors, "2023-03-01", IssueType.INVALID, line, col, literalPath, vse.isOk(), I18nConstants.SM_TARGET_TRANSLATE_BINDING_VSE_TARGET, vse.getError())) { + List systems = new ArrayList<>(); + if (srcVS != null) { + for (ConceptSetComponent inc : srcVS.getCompose().getInclude()) { + systems.add(inc.getSystem()); + } + } + List codes = ConceptMapUtilities.listTargets(cm, systems); + CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder(); + for (Coding code : codes) { + if (ValueSetUtilities.hasCodeInExpansion(vse.getValueset(), code)) { + b.append(code.getCode()); + } + } + if (b.count() > 0) { + warning(errors, "2023-03-01", IssueType.INVALID, line, col, literalPath, srcED.getBinding().hasValueSet() && srcED.getBinding().getStrength() == BindingStrength.REQUIRED, I18nConstants.SM_TARGET_TRANSLATE_BINDING_TARGET_WRONG, b.toString()); + } + } + } + } + } + return ok; } private String inferType(RuleInformation ruleInfo, VariableSet variables, Element rule, String transform, List params) {