Merge pull request #1140 from hapifhir/gg-202302-map-validation-3

structuremap validation and invariant fixes for forthcoming R5 release
This commit is contained in:
Grahame Grieve 2023-02-27 19:46:38 +11:00 committed by GitHub
commit b0daf66672
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 452 additions and 121 deletions

View File

@ -18,6 +18,7 @@ import org.hl7.fhir.r5.model.UriType;
import org.hl7.fhir.r5.model.ValueSet;
import org.hl7.fhir.r5.model.ValueSet.ConceptSetComponent;
import org.hl7.fhir.r5.model.ElementDefinition.TypeRefComponent;
import org.hl7.fhir.utilities.Utilities;
import org.hl7.fhir.utilities.VersionUtilities;
import org.hl7.fhir.utilities.npm.NpmPackage;
@ -115,6 +116,7 @@ public abstract class BaseLoaderR5 implements IContextResourceLoader {
cr.setUrl(patchUrl(cr.getUrl(), cr.fhirType()));
if (cr instanceof StructureDefinition) {
StructureDefinition sd = (StructureDefinition) cr;
sd.setBaseDefinition(patchUrl(sd.getBaseDefinition(), sd.fhirType()));
new ProfileUtilities(null, null, null, null).setIds(sd, false);
sd.addExtension().setUrl(URL_ELEMENT_DEF_NAMESPACE).setValue(new UriType(URL_BASE));
for (ElementDefinition ed : sd.getSnapshot().getElement())
@ -152,6 +154,9 @@ public abstract class BaseLoaderR5 implements IContextResourceLoader {
private void patchUrl(ElementDefinition ed) {
for (TypeRefComponent tr : ed.getType()) {
if (!Utilities.isAbsoluteUrl(tr.getCode())) {
tr.setCode(URL_BASE+versionString()+"/StructureDefinition/"+tr.getCode());
}
for (CanonicalType s : tr.getTargetProfile()) {
s.setValue(patchUrl(s.getValue(), "StructureDefinitino"));
}

View File

@ -291,10 +291,22 @@ public class Element extends Base {
for (Element child : children) {
if (name.equals(child.getName()))
return child.getValue();
}
for (Element child : children) {
if (name.equals(child.getNameBase()))
return child.getValue();
}
return null;
}
private String getNameBase() {
if (property.isChoice()) {
return property.getName().replace("[x]", "");
} else {
return getName();
}
}
public void setChildValue(String name, String value) {
if (children == null)
children = new ArrayList<Element>();
@ -543,6 +555,16 @@ public class Element extends Base {
Element ne = new Element(name, p);
children.add(ne);
return ne;
} else if (p.getDefinition().isChoice() && name.startsWith(p.getName().replace("[x]", ""))) {
String type = name.substring(p.getName().length()-3);
if (new ContextUtilities(property.getContext()).isPrimitiveDatatype(Utilities.uncapitalize(type))) {
type = Utilities.uncapitalize(type);
}
Element ne = new Element(name, p);
ne.setType(type);
children.add(ne);
return ne;
}
}

View File

@ -354,6 +354,10 @@ public class FmlParser extends ParserBase {
rule.forceElement("source").makeElement("variable").setValue(StructureMapUtilities.AUTO_VAR_NAME);
rule.forceElement("target").makeElement("variable").setValue(StructureMapUtilities.AUTO_VAR_NAME);
rule.forceElement("target").makeElement("transform").setValue(StructureMapTransform.CREATE.toCode());
Element dep = rule.forceElement("dependent");
dep.makeElement("name").setValue(StructureMapUtilities.DEF_GROUP_NAME);
dep.makeElement("parameter").makeElement("valueId").setValue(StructureMapUtilities.AUTO_VAR_NAME);
dep.makeElement("parameter").makeElement("valueId").setValue(StructureMapUtilities.AUTO_VAR_NAME);
// no dependencies - imply what is to be done based on types
}
if (newFmt) {
@ -480,7 +484,7 @@ public class FmlParser extends ParserBase {
loc = lexer.getCurrentLocation();
ExpressionNode node = fpe.parse(lexer);
target.setUserData(StructureMapUtilities.MAP_EXPRESSION, node);
target.addElement("parameter").markLocation(loc).setValue(node.toString());
target.addElement("parameter").markLocation(loc).makeElement("valueString").setValue(node.toString());
lexer.token(")");
} else if (lexer.hasToken("(")) {
target.makeElement("transform").markLocation(loc).setValue(name);
@ -529,11 +533,11 @@ public class FmlParser extends ParserBase {
private void parseParameter(Element ref, FHIRLexer lexer) throws FHIRLexerException, FHIRFormatError {
if (!lexer.isConstant()) {
ref.addElement("parameter").markLocation(lexer.getCurrentLocation()).setType("string").setValue(lexer.take());
ref.addElement("parameter").markLocation(lexer.getCurrentLocation()).makeElement("valueId").setValue(lexer.take());
} else if (lexer.isStringConstant())
ref.addElement("parameter").markLocation(lexer.getCurrentLocation()).setType("string").setValue(lexer.readConstant("??"));
ref.addElement("parameter").markLocation(lexer.getCurrentLocation()).makeElement("valueString").setValue(lexer.readConstant("??"));
else {
ref.addElement("parameter").markLocation(lexer.getCurrentLocation()).setType("string").setValue(readConstant(lexer.take(), lexer));
ref.addElement("parameter").markLocation(lexer.getCurrentLocation()).makeElement("valueString").setValue(readConstant(lexer.take(), lexer));
}
}

View File

@ -104,6 +104,7 @@ public class StructureMapUtilities {
public static final String MAP_EXPRESSION = "map.transform.expression";
private static final boolean RENDER_MULTIPLE_TARGETS_ONELINE = true;
public static final String AUTO_VAR_NAME = "vvv";
public static final String DEF_GROUP_NAME = "DefaultMappingGroupAnonymousAlias";
private final IWorkerContext worker;
private final FHIRPathEngine fpe;

View File

@ -782,7 +782,8 @@ public class I18nConstants {
public static final String SM_GROUP_INPUT_NO_TYPE = "SM_GROUP_INPUT_NO_TYPE";
public static final String SM_GROUP_INPUT_TYPE_NOT_DECLARED = "SM_GROUP_INPUT_TYPE_NOT_DECLARED";
public static final String SM_GROUP_INPUT_MODE_MISMATCH = "SM_GROUP_INPUT_MODE_MISMATCH";
public static final String SM_GROUP_INPUT_TYPE_UNKNOWN = "SM_GROUP_INPUT_TYPE_UNKNOWN";
public static final String SM_GROUP_INPUT_TYPE_UNKNOWN_STRUCTURE = "SM_GROUP_INPUT_TYPE_UNKNOWN_STRUCTURE";
public static final String SM_GROUP_INPUT_TYPE_UNKNOWN_TYPE = "SM_GROUP_INPUT_TYPE_UNKNOWN_TYPE";
public static final String SM_SOURCE_CONTEXT_UNKNOWN = "SM_SOURCE_CONTEXT_UNKNOWN";
public static final String SM_SOURCE_PATH_INVALID = "SM_SOURCE_PATH_INVALID";
public static final String SM_RULE_SOURCE_MIN_REDUNDANT = "SM_RULE_SOURCE_MIN_REDUNDANT";
@ -804,6 +805,13 @@ public class I18nConstants {
public static final String SM_TARGET_TRANSFORM_PARAM_UNPROCESSIBLE = "SM_TARGET_TRANSFORM_PARAM_UNPROCESSIBLE";
public static final String SM_TARGET_TRANSFORM_EXPRESSION_ERROR = "SM_TARGET_TRANSFORM_EXPRESSION_ERROR";
public static final String SM_IMPORT_NOT_FOUND = "SM_IMPORT_NOT_FOUND";
public static final String SM_TARGET_TYPE_MULTIPLE_POSSIBLE = "SM_TARGET_TYPE_MULTIPLE_POSSIBLE";
public static final String SM_DEPENDENT_PARAM_MODE_MISMATCH = "SM_DEPENDENT_PARAM_MODE_MISMATCH";
public static final String SM_DEPENDENT_PARAM_TYPE_MISMATCH = "SM_DEPENDENT_PARAM_TYPE_MISMATCH";
public static final String SM_ORPHAN_GROUP = "SM_ORPHAN_GROUP";
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";
}

View File

@ -830,9 +830,10 @@ SM_NAME_INVALID = The name {0} is not valid
SM_GROUP_INPUT_DUPLICATE = The name {0} is already used
SM_GROUP_INPUT_MODE_INVALID = The group parameter {0} mode {1} isn''t valid
SM_GROUP_INPUT_NO_TYPE = The group parameter {0} has no type, so the paths cannot be validated
SM_GROUP_INPUT_TYPE_NOT_DECLARED = The type {0} was not declared and is unknown
SM_GROUP_INPUT_TYPE_NOT_DECLARED = The type {0} is not declared and is unknown
SM_GROUP_INPUT_MODE_MISMATCH = The type {0} has mode {1} which doesn''t match the structure definition {2}
SM_GROUP_INPUT_TYPE_UNKNOWN = The type {0} which maps to the canonical URL {1} is not known, so the paths cannot be validated
SM_GROUP_INPUT_TYPE_UNKNOWN_STRUCTURE = The type {0} which maps to the canonical URL {1} is not known, so the paths cannot be validated
SM_GROUP_INPUT_TYPE_UNKNOWN_TYPE = The type {0} is not known, so the paths cannot be validated
SM_SOURCE_CONTEXT_UNKNOWN = The source context {0} is not known at this point
SM_SOURCE_PATH_INVALID = The source path {0}.{1} refers to the path {2} which is unknown
SM_RULE_SOURCE_MIN_REDUNDANT = The min value of {0} is redundant since the valid min is {0}
@ -854,5 +855,14 @@ SM_TARGET_TRANSFORM_TYPE_UNPROCESSIBLE = The value of the type parameter could n
SM_TARGET_TRANSFORM_PARAM_UNPROCESSIBLE = The parameter at index {0} could not be processed (type = {1})
SM_TARGET_TRANSFORM_EXPRESSION_ERROR = The FHIRPath expression passed as the evaluate parameter is invalid: {0}
SM_IMPORT_NOT_FOUND = No maps were found to match {0} - validation may be wrong
SM_TARGET_TYPE_MULTIPLE_POSSIBLE = Multiple types are possible here ({0}) so further type checking is not possible
SM_DEPENDENT_PARAM_MODE_MISMATCH = The parameter {0} refers to the variable {1} but it''s mode is {2} which is not the same as the mode required for the group {3}
SM_DEPENDENT_PARAM_TYPE_MISMATCH = The parameter {0} refers to the variable {1} but it''s type is {2} which is not compatible with the type required for the group {3}
SM_ORPHAN_GROUP = This group is not called from within this mapping script, and does not have types on it's inputs, so type verification is not possible
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}

View File

@ -3774,7 +3774,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
}
TypedElementDefinition ted = null;
String fp = FHIRPathExpressionFixer.fixExpr(discriminator, null);
String fp = FHIRPathExpressionFixer.fixExpr(discriminator, null, context.getVersion());
ExpressionNode expr = null;
try {
expr = fpe.parse(fp);
@ -4392,7 +4392,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
}
try {
n = fpe.parse(FHIRPathExpressionFixer.fixExpr(expression.toString(), null));
n = fpe.parse(FHIRPathExpressionFixer.fixExpr(expression.toString(), null, context.getVersion()));
} catch (FHIRLexerException e) {
if (STACK_TRACE) e.printStackTrace();
throw new FHIRException(context.formatMessage(I18nConstants.PROBLEM_PROCESSING_EXPRESSION__IN_PROFILE__PATH__, expression, profile.getVersionedUrl(), path, e.getMessage()));
@ -6020,7 +6020,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
}
List<ValidationMessage> invErrors = null;
// We key based on inv.expression rather than inv.key because expressions can change in derived profiles and aren't guaranteed to be consistent across profiles.
String key = FHIRPathExpressionFixer.fixExpr(inv.getExpression(), inv.getKey());
String key = FHIRPathExpressionFixer.fixExpr(inv.getExpression(), inv.getKey(), context.getVersion());
if (!invMap.keySet().contains(key)) {
invErrors = new ArrayList<ValidationMessage>();
invMap.put(key, invErrors);
@ -6074,7 +6074,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
if (n == null) {
long t = System.nanoTime();
try {
String expr = FHIRPathExpressionFixer.fixExpr(inv.getExpression(), inv.getKey());
String expr = FHIRPathExpressionFixer.fixExpr(inv.getExpression(), inv.getKey(), context.getVersion());
n = fpe.parse(expr);
} catch (FHIRException e) {
rule(errors, NO_RULE_DATE, IssueType.INVARIANT, element.line(), element.col(), path, false, I18nConstants.PROBLEM_PROCESSING_EXPRESSION__IN_PROFILE__PATH__, inv.getExpression(), profile.getVersionedUrl(), path, e.getMessage());
@ -6285,7 +6285,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
try {
ExpressionNode n = (ExpressionNode) inv.getUserData("validator.expression.cache");
if (n == null) {
n = fpe.parse(FHIRPathExpressionFixer.fixExpr(inv.getExpression(), inv.getKey()));
n = fpe.parse(FHIRPathExpressionFixer.fixExpr(inv.getExpression(), inv.getKey(), context.getVersion()));
inv.setUserData("validator.expression.cache", n);
}
fpe.check(null, sd.getKind() == StructureDefinitionKind.RESOURCE ? sd.getType() : "DomainResource", ed.getPath(), n);

View File

@ -1,66 +1,37 @@
package org.hl7.fhir.validation.instance.type;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.hl7.fhir.convertors.factory.VersionConvertorFactory_10_50;
import org.hl7.fhir.convertors.factory.VersionConvertorFactory_14_50;
import org.hl7.fhir.convertors.factory.VersionConvertorFactory_30_50;
import org.hl7.fhir.convertors.factory.VersionConvertorFactory_40_50;
import org.hl7.fhir.exceptions.DefinitionException;
import org.hl7.fhir.exceptions.FHIRException;
import org.hl7.fhir.exceptions.PathEngineException;
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.context.IWorkerContext.ValidationResult;
import org.hl7.fhir.r5.elementmodel.Element;
import org.hl7.fhir.r5.elementmodel.Manager;
import org.hl7.fhir.r5.elementmodel.Manager.FhirFormat;
import org.hl7.fhir.r5.formats.IParser.OutputStyle;
import org.hl7.fhir.r5.model.Coding;
import org.hl7.fhir.r5.model.ElementDefinition;
import org.hl7.fhir.r5.model.ElementDefinition.TypeRefComponent;
import org.hl7.fhir.r5.model.ExpressionNode;
import org.hl7.fhir.r5.model.Extension;
import org.hl7.fhir.r5.model.Resource;
import org.hl7.fhir.r5.model.StructureDefinition;
import org.hl7.fhir.r5.model.StructureDefinition.StructureDefinitionKind;
import org.hl7.fhir.r5.model.StructureDefinition.TypeDerivationRule;
import org.hl7.fhir.r5.model.StructureMap;
import org.hl7.fhir.r5.model.StructureMap.StructureMapGroupComponent;
import org.hl7.fhir.r5.model.StructureMap.StructureMapGroupInputComponent;
import org.hl7.fhir.r5.model.StructureMap.StructureMapGroupTypeMode;
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.utils.FHIRPathEngine;
import org.hl7.fhir.r5.utils.ToolingExtensions;
import org.hl7.fhir.r5.utils.XVerExtensionManager;
import org.hl7.fhir.r5.utils.FHIRLexer.FHIRLexerException;
import org.hl7.fhir.r5.utils.FHIRPathEngine.ElementDefinitionMatch;
import org.hl7.fhir.r5.utils.structuremap.StructureMapUtilities;
import org.hl7.fhir.utilities.CommaSeparatedStringBuilder;
import org.hl7.fhir.utilities.Utilities;
import org.hl7.fhir.utilities.VersionUtilities;
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.type.StructureMapValidator.ElementDefinitionSource;
import org.hl7.fhir.validation.instance.type.StructureMapValidator.RuleInformation;
import org.hl7.fhir.validation.instance.type.StructureMapValidator.VariableDefn;
import org.hl7.fhir.validation.instance.type.StructureMapValidator.VariableSet;
import org.hl7.fhir.validation.instance.utils.NodeStack;
import org.jvnet.jaxb2_commons.xml.bind.annotation.adapters.CommaDelimitedStringAdapter;
public class StructureMapValidator extends BaseValidator {
@ -84,6 +55,7 @@ public class StructureMapValidator extends BaseValidator {
public class RuleInformation {
int maxCount = 1;
String defVariable;
public void seeCardinality(int max) {
if (max == Integer.MAX_VALUE || maxCount == Integer.MAX_VALUE) {
@ -101,7 +73,13 @@ public class StructureMapValidator extends BaseValidator {
return maxCount;
}
public String getDefVariable() {
return defVariable;
}
public void setDefVariable(String defVariable) {
this.defVariable = defVariable;
}
}
public class VariableDefn {
@ -169,6 +147,16 @@ public class StructureMapValidator extends BaseValidator {
this.type = type;
}
public String getWorkingType() {
if (type != null) {
return type;
}
if (ed != null && ed.getType().size() == 1) {
return ed.getType().get(0).getWorkingCode();
}
return null;
}
}
public class VariableSet {
@ -222,7 +210,11 @@ public class StructureMapValidator extends BaseValidator {
return null;
}
public void add(String pname, VariableDefn v) {
VariableDefn vn = v.copy();
vn.name = pname;
list.add(vn);
}
}
private static final boolean SOURCE = true;
@ -254,14 +246,44 @@ public class StructureMapValidator extends BaseValidator {
}
List<Element> groups = src.getChildrenByName("group");
// we iterate the groups repeatedly, validating them if they have stated types or found types, until nothing happens
boolean fired = false;
do {
fired = false;
cc = 0;
for (Element group : groups) {
if (!group.hasUserData("structuremap.validated")) {
if (hasInputTypes(group) || group.hasUserData("structuremap.parameters")) {
group.setUserData("structuremap.validated", true);
fired = true;
ok = validateGroup(errors, src, group, stack.push(group, cc, null, null)) && ok;
}
}
cc++;
}
} while (fired);
cc = 0;
for (Element group : groups) {
if (!group.hasUserData("structuremap.validated")) {
hint(errors, "2023-03-01", IssueType.INFORMATIONAL, group.line(), group.col(), stack.getLiteralPath(), ok, I18nConstants.SM_ORPHAN_GROUP);
ok = validateGroup(errors, src, group, stack.push(group, cc, null, null)) && ok;
}
cc++;
}
return ok;
}
private boolean hasInputTypes(Element group) {
List<Element> inputs = group.getChildrenByName("input");
for (Element input : inputs) {
if (!input.hasChild("type")) {
return false;
}
}
return true;
}
private boolean validateImport(List<ValidationMessage> errors, Element src, Element import_, NodeStack stack) {
String url = import_.primitiveValue();
boolean ok = false;
@ -293,13 +315,14 @@ public class StructureMapValidator extends BaseValidator {
}
VariableSet variables = new VariableSet();
VariableSet pvars = (VariableSet) group.getUserData("structuremap.parameters");
// first, load all the inputs
List<Element> inputs = group.getChildrenByName("input");
List<Element> structures = src.getChildrenByName("structure");
int cc = 0;
for (Element input : inputs) {
ok = validateInput(errors, src, group, input, stack.push(input, cc, null, null), structures, variables) && ok;
ok = validateInput(errors, src, group, input, stack.push(input, cc, null, null), structures, variables, pvars) && ok;
cc++;
}
@ -337,6 +360,7 @@ public class StructureMapValidator extends BaseValidator {
private StructureMapGroupComponent makeGroupComponent(Element group) {
StructureMapGroupComponent grp = new StructureMapGroupComponent();
grp.setUserData("element.source", group);
grp.setName(group.getChildValue("name"));
List<Element> inputs = group.getChildrenByName("input");
for (Element input : inputs) {
@ -352,27 +376,55 @@ public class StructureMapValidator extends BaseValidator {
return grp;
}
private boolean validateInput(List<ValidationMessage> errors, Element src, Element group, Element input, NodeStack stack, List<Element> structures, VariableSet variables) {
private boolean validateInput(List<ValidationMessage> errors, Element src, Element group, Element input, NodeStack stack, List<Element> structures, VariableSet variables, VariableSet pvars) {
boolean ok = false;
String name = input.getChildValue("name");
String type = input.getChildValue("type");
String mode = input.getChildValue("mode");
String type = input.getChildValue("type");
VariableDefn pv = null;
if (type == null && pvars != null) {
pv = pvars.getVariable(name, mode.equals("source"));
if (pv != null) {
type = pv.getWorkingType();
}
}
if (rule(errors, "2023-03-01", IssueType.NOTSUPPORTED, input.line(), input.col(), stack.getLiteralPath(), idIsValid(name), I18nConstants.SM_NAME_INVALID, name) && // the name {0} is not valid)
rule(errors, "2023-03-01", IssueType.DUPLICATE, input.line(), input.col(), stack.getLiteralPath(), !variables.hasVariable(name), I18nConstants.SM_GROUP_INPUT_DUPLICATE, name)) { // the name {0} is not valid)
VariableDefn v = variables.add(name, mode);
if (rule(errors, "2023-03-01", IssueType.INVALID, input.line(), input.col(), stack.getLiteralPath(), Utilities.existsInList(mode, "source", "target"), I18nConstants.SM_GROUP_INPUT_MODE_INVALID, name, mode) && // the group parameter {0} mode {1} isn't valid
warning(errors, "2023-03-01", IssueType.NOTSUPPORTED, input.line(), input.col(), stack.getLiteralPath(), type != null, I18nConstants.SM_GROUP_INPUT_NO_TYPE, name)) { // the group parameter {0} has no type, so the paths cannot be validated
String smode = null;
StructureDefinition sd = null;
ElementDefinition ed = null;
if (pv != null) {
sd = pv.getSd();
ed = pv.getEd();
} else {
Element structure = findStructure(structures, type);
if (rule(errors, "2023-03-01", IssueType.NOTSUPPORTED, input.line(), input.col(), stack.getLiteralPath(), structure != null, I18nConstants.SM_GROUP_INPUT_TYPE_NOT_DECLARED, type)) { // the type {0} was not declared and is unknown
if (structure != null) {
smode = structure.getChildValue("mode");
String url = structure.getChildValue("url");
String smode = structure.getChildValue("mode");
StructureDefinition sd = context.fetchResource(StructureDefinition.class, url);
if (rule(errors, "2023-03-01", IssueType.NOTSUPPORTED, input.line(), input.col(), stack.getLiteralPath(), mode.equals(smode), I18nConstants.SM_GROUP_INPUT_MODE_MISMATCH, type, mode, smode) && // the type {0} has mode {1} which doesn't match the structure definition {2}
rule(errors, "2023-03-01", IssueType.INVALID, input.line(), input.col(), stack.getLiteralPath(), sd != null, I18nConstants.SM_GROUP_INPUT_TYPE_UNKNOWN, type, url)) { // the type {0} which maps to the canonical URL {1} is not known, so the paths cannot be validated
v.setType(1, sd, sd.getSnapshot().getElementFirstRep(), null);
ok = true;
sd = context.fetchResource(StructureDefinition.class, url);
if (sd == null) {
rule(errors, "2023-03-01", IssueType.INVALID, input.line(), input.col(), stack.getLiteralPath(), sd != null, I18nConstants.SM_GROUP_INPUT_TYPE_UNKNOWN_STRUCTURE, type, url);
}
} else if (type != null) {
sd = context.fetchTypeDefinition(type);
if (sd == null) {
rule(errors, "2023-03-01", IssueType.INVALID, input.line(), input.col(), stack.getLiteralPath(), sd != null, I18nConstants.SM_GROUP_INPUT_TYPE_UNKNOWN_TYPE, type);
}
} else {
rule(errors, "2023-03-01", IssueType.NOTSUPPORTED, input.line(), input.col(), stack.getLiteralPath(), structure != null, I18nConstants.SM_GROUP_INPUT_TYPE_NOT_DECLARED, type);
ok = false;
}
if (sd != null) {
ed = sd.getSnapshot().getElementFirstRep();
}
}
if (rule(errors, "2023-03-01", IssueType.NOTSUPPORTED, input.line(), input.col(), stack.getLiteralPath(), smode == null || mode.equals(smode), I18nConstants.SM_GROUP_INPUT_MODE_MISMATCH, type, mode, smode)) { // the type {0} has mode {1} which doesn't match the structure definition {2}
v.setType(1, sd, ed, null);
ok = true;
}
}
}
@ -403,7 +455,7 @@ public class StructureMapValidator extends BaseValidator {
List<Element> sources = rule.getChildrenByName("source");
int cc = 0;
for (Element source : sources) {
ok = validateRuleSource(errors, src, group, rule, source, stack.push(source, cc, null, null), lvars, ruleInfo) && ok;
ok = validateRuleSource(errors, src, group, rule, source, stack.push(source, cc, null, null), lvars, ruleInfo, cc) && ok;
cc++;
}
// process the targets
@ -431,8 +483,11 @@ public class StructureMapValidator extends BaseValidator {
return ok;
}
private boolean validateRuleSource(List<ValidationMessage> errors, Element src, Element group, Element rule, Element source, NodeStack stack, VariableSet variables, RuleInformation ruleInfo) {
private boolean validateRuleSource(List<ValidationMessage> errors, Element src, Element group, Element rule, Element source, NodeStack stack, VariableSet variables, RuleInformation ruleInfo, int loopCounter) {
String context = source.getChildValue("context");
if (loopCounter > 0) {
ruleInfo.setDefVariable(null);
}
boolean ok = rule(errors, "2023-03-01", IssueType.INVALID, source.line(), source.col(), stack.getLiteralPath(), idIsValid(context), I18nConstants.SM_NAME_INVALID, context) &&
rule(errors, "2023-03-01", IssueType.UNKNOWN, source.line(), source.col(), stack.getLiteralPath(), variables.hasVariable(context, SOURCE), I18nConstants.SM_SOURCE_CONTEXT_UNKNOWN, context);
if (ok) {
@ -449,6 +504,9 @@ public class StructureMapValidator extends BaseValidator {
if (hint(errors, "2023-03-01", IssueType.INVALID, source.line(), source.col(), stack.getLiteralPath(), variable != null, I18nConstants.SM_RULE_SOURCE_UNASSIGNED)) {
if (rule(errors, "2023-03-01", IssueType.INVALID, source.line(), source.col(), stack.getLiteralPath(), idIsValid(variable), I18nConstants.SM_NAME_INVALID, variable)) {
vn = variables.add(variable, v.getMode()); // may overwrite
if (loopCounter == 0) {
ruleInfo.setDefVariable(variable);
}
} else {
ok = false;
}
@ -545,17 +603,37 @@ public class StructureMapValidator extends BaseValidator {
if (rule(errors, "2023-03-01", IssueType.INVALID, target.line(), target.col(), stack.getLiteralPath(), !els.isEmpty(), I18nConstants.SM_TARGET_PATH_INVALID, context, element, v.getEd().getPath()+"."+element)) {
if (warning(errors, "2023-03-01", IssueType.INVALID, target.line(), target.col(), stack.getLiteralPath(), els.size() == 1, I18nConstants.SM_TARGET_PATH_MULTIPLE_MATCHES, context, element, v.getEd().getPath()+"."+element, render(els))) {
ElementDefinitionSource el = els.get(0);
String type = null; // maybe inferred / derived from transform in the future
String transform = target.getChildValue("transform");
List<Element> params = target.getChildren("parameter");
if (transform == null) {
transform = "create"; // implied
rule(errors, "2023-03-01", IssueType.INVALID, target.line(), target.col(), stack.getLiteralPath(), params.size() == 0, I18nConstants.SM_TARGET_NO_TRANSFORM_NO_CHECKED, transform);
}
// List<String> types = listTypes(el.getEd().getType());
String type = null;
if (el.getEd().getType().size() == 1) {
type = el.getEd().getTypeFirstRep().getWorkingCode();
} else {
type = inferType(ruleInfo, variables, rule, transform, params);
}
switch (transform) {
case "create":
if (rule(errors, "2023-03-01", IssueType.INVALID, target.line(), target.col(), stack.getLiteralPath(), params.size() < 2, I18nConstants.SM_TARGET_TRANSFORM_PARAM_COUNT_RANGE, "create", "0", "1", params.size())) {
if (params.size() == 1) {
type = params.get(0).primitiveValue();
type = params.get(0).getChildValue("value");
warning(errors, "2023-03-01", IssueType.INVALID, target.line(), target.col(), stack.getLiteralPath(),type != null, I18nConstants.SM_TARGET_TRANSFORM_TYPE_UNPROCESSIBLE);
} else {
// maybe can guess? maybe not ... type =
}
} else {
ok = false;
}
break;
case "copy": // logic is the same as create?
if (rule(errors, "2023-03-01", IssueType.INVALID, target.line(), target.col(), stack.getLiteralPath(), params.size() < 2, I18nConstants.SM_TARGET_TRANSFORM_PARAM_COUNT_RANGE, "create", "0", "1", params.size())) {
if (params.size() == 1) {
type = params.get(0).getChildValue("value");
warning(errors, "2023-03-01", IssueType.INVALID, target.line(), target.col(), stack.getLiteralPath(),type != null, I18nConstants.SM_TARGET_TRANSFORM_TYPE_UNPROCESSIBLE);
} else {
// maybe can guess? maybe not ... type =
@ -573,7 +651,7 @@ public class StructureMapValidator extends BaseValidator {
break;
case "evaluate":
if (rule(errors, "2023-03-01", IssueType.INVALID, target.line(), target.col(), stack.getLiteralPath(), params.size() == 1, I18nConstants.SM_TARGET_TRANSFORM_PARAM_COUNT_SINGLE, "evaluate", "1", params.size())) {
String exp = params.get(0).primitiveValue();
String exp = params.get(0).getChildValue("value");
if (rule(errors, "2023-03-01", IssueType.INVALID, params.get(0).line(), params.get(0).col(), stack.getLiteralPath(), exp != null, I18nConstants.SM_TARGET_TRANSFORM_PARAM_UNPROCESSIBLE, "0", params.size())) {
try {
TypeDetails td = fpe.check(null, v.getSd().getType(), v.getEd().getPath(), fpe.parse(exp));
@ -594,11 +672,13 @@ public class StructureMapValidator extends BaseValidator {
rule(errors, "2023-03-01", IssueType.INVALID, target.line(), target.col(), stack.getLiteralPath(), false, I18nConstants.SM_TARGET_TRANSFORM_NOT_CHECKED, transform);
ok = false;
}
}
//todo: transform / parameter
if (vn != null) {
// it's just a warning: maybe this'll work out at run time?
warning(errors, "2023-03-01", IssueType.INVALID, target.line(), target.col(), stack.getLiteralPath(), type != null, I18nConstants.SM_TARGET_TYPE_MULTIPLE_POSSIBLE, el.getEd().typeSummary());
vn.setType(ruleInfo.getMaxCount(), el.getSd(), el.getEd(), type); // may overwrite
}
}
} else {
ok = false;
@ -610,6 +690,79 @@ public class StructureMapValidator extends BaseValidator {
return ok;
}
private String inferType(RuleInformation ruleInfo, VariableSet variables, Element rule, String transform, List<Element> params) {
// under some special conditions, we can infer what the type will be:
// * there's a nominated default variable
// * that variable has as single type
// * there's a create with no param
// * there's a single dependent rule with name = StructureMapUtilities.DEF_GROUP_NAME
// * there's a default type group for the type of the source type
// otherwise, we can't know the target type.
if (ruleInfo.getDefVariable() != null && "create".equals(transform) && params.isEmpty()) {
VariableDefn v = variables.getVariable(ruleInfo.getDefVariable(), SOURCE);
if (v != null && (v.getEd().getType().size() == 1 || v.getType() != null)) {
List<Element> dependents = rule.getChildrenByName("dependent");
if (dependents.size() == 1 && StructureMapUtilities.DEF_GROUP_NAME.equals(dependents.get(0).getChildValue("name"))) {
String type = v.getType() != null ? v.getType() : v.getEd().getTypeFirstRep().getWorkingCode();
// now, we look for a default group.
// todo: look in this source
// now look through the inputs
for (StructureMap map : imports) {
for (StructureMapGroupComponent grp : map.getGroup()) {
if (grp.getTypeMode() == StructureMapGroupTypeMode.TYPEANDTYPES && grp.getInput().size() == 2) {
String grpType = getTypeForGroupInput(map, grp, grp.getInput().get(0));
if (sameTypes(type, grpType)) {
String tgtType = getTypeForGroupInput(map, grp, grp.getInput().get(1));
if (tgtType != null) {
return tgtType;
}
}
}
}
}
}
}
}
return null;
}
private boolean sameTypes(String type1, String type2) {
if (type1 == null || type2 == null) {
return false;
}
if (!Utilities.isAbsoluteUrl(type1)) {
type1 = "http://hl7.org/fhir/StructureDefinition/"+type1;
}
if (!Utilities.isAbsoluteUrl(type2)) {
type2 = "http://hl7.org/fhir/StructureDefinition/"+type2;
}
return type1.equals(type2);
}
private String getTypeForGroupInput(StructureMap map, StructureMapGroupComponent grp, StructureMapGroupInputComponent input) {
String type = input.getType();
StructureMapModelMode mode = input.getMode() == StructureMapInputMode.SOURCE ? StructureMapModelMode.SOURCE : StructureMapModelMode.TARGET;
if (input == null) {
return null;
}
for (StructureMapStructureComponent st : map.getStructure()) {
if (type.equals(st.getAlias()) && mode == st.getMode()) {
return st.getUrl();
}
}
return type;
}
private List<String> listTypes(List<TypeRefComponent> types) {
List<String> res = new ArrayList<>();
for (TypeRefComponent td : types) {
res.add(td.getWorkingCode());
}
Collections.sort(res);
return res;
}
private String render(List<ElementDefinitionSource> list) {
CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder();
for (ElementDefinitionSource t : list) {
@ -668,16 +821,138 @@ public class StructureMapValidator extends BaseValidator {
}
private boolean validateDependent(List<ValidationMessage> errors, Element src, Element group, Element dependent, NodeStack stack, VariableSet lvars) {
private boolean validateDependent(List<ValidationMessage> errors, Element src, Element group, Element dependent, NodeStack stack, VariableSet variables) {
boolean ok = true;
String name = dependent.getChildValue("name");
StructureMapGroupComponent grp = resolveGroup(name, src);
if (rule(errors, "2023-03-01", IssueType.NOTSUPPORTED, dependent.line(), dependent.col(), stack.push(dependent, -1, null, null).getLiteralPath(), grp != null, I18nConstants.SM_RULEGROUP_NOT_FOUND, name)) {
// check inputs
if (StructureMapUtilities.DEF_GROUP_NAME.equals(name)) {
VariableDefn srcVar = variables.getVariable(StructureMapUtilities.AUTO_VAR_NAME, true);
VariableDefn tgtVar = variables.getVariable(StructureMapUtilities.AUTO_VAR_NAME, false);
if (srcVar != null && srcVar.hasTypeInfo() && tgtVar != null && tgtVar.hasTypeInfo()) {
String srcType = srcVar.getWorkingType();
String tgtType = tgtVar.getWorkingType();
if (rule(errors, "2023-03-01", IssueType.NOTFOUND, dependent.line(), dependent.col(), stack.getLiteralPath(), srcType != null, I18nConstants.SM_SOURCE_TYPE_NOT_FOUND) &&
rule(errors, "2023-03-01", IssueType.NOTFOUND, dependent.line(), dependent.col(), stack.getLiteralPath(), tgtType != null, I18nConstants.SM_TARGET_TYPE_NOT_FOUND)) {
StructureMapGroupComponent grp = findDefaultGroup(src, srcType, tgtType);
ok = rule(errors, "2023-03-01", IssueType.NOTFOUND, dependent.line(), dependent.col(), stack.getLiteralPath(), grp != null, I18nConstants.SM_MATCHING_RULEGROUP_NOT_FOUND, srcType, tgtType) && ok;
} else {
ok = false;
}
}
} else {
StructureMapGroupComponent grp = resolveGroup(name, src);
if (rule(errors, "2023-03-01", IssueType.NOTFOUND, dependent.line(), dependent.col(), stack.getLiteralPath(), grp != null, I18nConstants.SM_RULEGROUP_NOT_FOUND, name)) {
List<Element> params = dependent.getChildren("parameter");
if (rule(errors, "2023-03-01", IssueType.INVALID, dependent.line(), dependent.col(), stack.getLiteralPath(), params.size() == grp.getInput().size(), I18nConstants.SM_RULEGROUP_NOT_FOUND, params.size(), grp.getInput().size())) {
VariableSet lvars = new VariableSet();
int cc = 0;
for (Element param : params) {
NodeStack pstack = stack.push(param, cc, null, null);
StructureMapGroupInputComponent input = grp.getInput().get(cc);
String pname = input.getName();
VariableDefn v = getParameter(errors, param, pstack, variables, input.getMode());
if (v != null) {
if (rule(errors, "2023-03-01", IssueType.INVALID, param.line(), param.col(), pstack.getLiteralPath(), v.mode.equals(input.getMode().toCode()), I18nConstants.SM_DEPENDENT_PARAM_MODE_MISMATCH, param.getChildValue("name"), v.mode, input.getMode().toCode()) &&
rule(errors, "2023-03-01", IssueType.INVALID, param.line(), param.col(), pstack.getLiteralPath(), typesMatch(v, input.getType()), I18nConstants.SM_DEPENDENT_PARAM_TYPE_MISMATCH, param.getChildValue("name"), v, input.getType())) {
lvars.add(pname, v);
} else {
ok = false;
}
} else {
ok = false;
}
cc++;
}
if (ok && grp.hasUserData("element.source")) {
Element g = (Element) grp.getUserData("element.source");
if (g.hasUserData("structuremap.parameters")) {
throw new Error("bang! - this situation is not handled");
} else {
g.setUserData("structuremap.parameters", lvars);
}
}
}
} else {
ok = false;
}
}
return ok;
}
private StructureMapGroupComponent findDefaultGroup(Element src, String srcType, String tgtType) {
List<Element> groups = src.getChildrenByName("group");
for (Element group : groups) {
if (Utilities.existsInList(group.getChildValue("typeMode"), "types", "type-and-types")) {
List<Element> inputs = group.getChildrenByName("input");
if (inputs.size() == 2 && "source".equals(inputs.get(0).getChildValue("mode")) && "source".equals(inputs.get(0).getChildValue("mode"))) {
String srcT = resolveInputType(src, inputs.get(0));
String tgtT = resolveInputType(src, inputs.get(1));
if (sameTypes(srcT, srcType) && sameTypes(tgtT, tgtType)) {
return makeGroupComponent(group);
}
}
}
}
for (StructureMap map : imports) {
for (StructureMapGroupComponent grp : map.getGroup()) {
if ((grp.getTypeMode() == StructureMapGroupTypeMode.TYPES || grp.getTypeMode() == StructureMapGroupTypeMode.TYPEANDTYPES) &&
grp.getInput().size() == 2 && grp.getInput().get(0).getMode() == StructureMapInputMode.SOURCE && grp.getInput().get(1).getMode() == StructureMapInputMode.TARGET) {
String srcT = resolveInputType(map, grp.getInput().get(0));
String tgtT = resolveInputType(map, grp.getInput().get(1));
if (sameTypes(srcT, srcType) && sameTypes(tgtT, tgtType)) {
return grp;
}
}
}
}
return null;
}
private String resolveInputType(StructureMap map, StructureMapGroupInputComponent input) {
String type = input.getType();
if (type == null) {
return null;
}
for (StructureMapStructureComponent structure : map.getStructure()) {
if (type.equals(structure.getAlias())) {
return structure.getUrl();
}
}
StructureDefinition sd = context.fetchTypeDefinition(type);
return sd == null ? null : sd.getUrl();
}
private String resolveInputType(Element src, Element input) {
String type = input.getChildValue("type");
if (type == null) {
return null;
}
for (Element structure : input.getChildren("structure")) {
if (type.equals(structure.getChildValue("alias"))) {
return structure.getChildValue("url");
}
}
StructureDefinition sd = context.fetchTypeDefinition(type);
return sd == null ? null : sd.getUrl();
}
private boolean typesMatch(VariableDefn v, String type) {
if (type == null) {
return true;
} else {
return false;
}
}
private VariableDefn getParameter(List<ValidationMessage> errors, Element param, NodeStack pstack, VariableSet variables, StructureMapInputMode mode) {
Element v = param.getNamedChild("value");
if (v.fhirType().equals("id")) {
return variables.getVariable(v.primitiveValue(), mode == StructureMapInputMode.SOURCE);
} else {
String type = v.fhirType();
StructureDefinition sd = context.fetchTypeDefinition(type);
return new VariableDefn("$", "source").setType(1, sd, sd.getSnapshot().getElementFirstRep(), null);
}
}
}

View File

@ -1,16 +1,22 @@
package org.hl7.fhir.validation.instance.utils;
import org.hl7.fhir.utilities.VersionUtilities;
public class FHIRPathExpressionFixer {
public static String fixExpr(String expr, String key) {
public static String fixExpr(String expr, String key, String version) {
// this is a hack work around for past publication of wrong FHIRPath expressions
// R4
// waiting for 4.0.2
boolean r5 = VersionUtilities.isR5Ver(version);
// if (r5) {
// return expr;
// }
//TODO is this expression below correct? @grahamegrieve
if ("probability is decimal implies (probability as decimal) <= 100".equals(expr)) {
return "probability.empty() or ((probability is decimal) implies ((probability as decimal) <= 100))";
}
// if ("probability is decimal implies (probability as decimal) <= 100".equals(expr)) {
// return "probability.empty() or ((probability is decimal) implies ((probability as decimal) <= 100))";
// }
if ("enableWhen.count() > 2 implies enableBehavior.exists()".equals(expr)) {
return "enableWhen.count() >= 2 implies enableBehavior.exists()";
}
@ -52,10 +58,10 @@ public class FHIRPathExpressionFixer {
}
// clarification in FHIRPath spec
if ("eld-19".equals(key)) {
if (!r5 && "eld-19".equals(key)) {
return "path.matches('^[^\\\\s\\\\.,:;\\\\\\'\"\\\\/|?!@#$%&*()\\\\[\\\\]{}]{1,64}(\\\\.[^\\\\s\\\\.,:;\\\\\\'\"\\\\/|?!@#$%&*()\\\\[\\\\]{}]{1,64}(\\\\[x\\\\])?(\\\\:[^\\\\s\\\\.]+)?)*$')";
}
if ("eld-20".equals(key)) {
if (!r5 && "eld-20".equals(key)) {
return "path.matches('^[A-Za-z][A-Za-z0-9]*(\\\\.[a-z][A-Za-z0-9]*(\\\\[x])?)*$')";
}