From 92c02d229452547e6a1270810eb058ebbc71cd76 Mon Sep 17 00:00:00 2001 From: Grahame Grieve Date: Sat, 4 Jul 2020 08:25:15 +1000 Subject: [PATCH] Add support for validating against ImplementationGuide.global (#266) * fix for new test case about profiles restricting types * update release notes * fix NPEs doing ProfileComparison * fix non-translated messages in InstanceValidator * Fix validation issues * update to 1.1.22 tests * fix bug finding children in getChildList when element is a reference to another element * fixes to QuestionnaireRenderer + add QuestionnaireResponseRenderer * Add more informative error when no type parser provided * fix bug where current/dev builds do not properly update the cache * improve test error message * set up release notes * fix bug with CDA snapshot generation * fix bug in type checking code * Add support for validating against ImplementationGuide.global * Fix bug in QuestionnaireResponse rendering * update tests dependency * ping build * Add additional StructureDefinition validation * update release notes --- RELEASE_NOTES.md | 6 + .../fhir/r5/conformance/ProfileUtilities.java | 5 +- .../hl7/fhir/r5/renderers/BundleRenderer.java | 4 +- .../QuestionnaireResponseRenderer.java | 4 +- .../hl7/fhir/r5/utils/IResourceValidator.java | 9 +- .../fhir/utilities/i18n/I18nConstants.java | 7 +- .../utilities/tests/BaseTestingUtilities.java | 1 + .../src/main/resources/Messages.properties | 7 +- .../hl7/fhir/validation/BaseValidator.java | 2 +- .../hl7/fhir/validation/ValidationEngine.java | 17 ++- .../fhir/validation/cli/model/CliContext.java | 11 ++ .../cli/services/ValidationService.java | 1 + .../hl7/fhir/validation/cli/utils/Params.java | 5 +- .../instance/InstanceValidator.java | 37 +++++- .../instance/type/BundleValidator.java | 2 +- .../type/SearchParameterValidator.java | 65 +++++++++- .../type/StructureDefinitionValidator.java | 115 ++++++++++++++++++ .../validation/profile/ProfileValidator.java | 2 +- .../validation/tests/ValidationTestSuite.java | 10 ++ pom.xml | 2 +- 20 files changed, 294 insertions(+), 18 deletions(-) create mode 100644 org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/type/StructureDefinitionValidator.java diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index e69de29bb..323913395 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -0,0 +1,6 @@ +* Added text/cql.identifier media type to Measure validation +* Fix bug in QuestionnaireResponse rendering +* Validate based on ImplementationGuide.global +* add validation parameter -crumb-trails +* improve validation of StructureDefinitions + diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/conformance/ProfileUtilities.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/conformance/ProfileUtilities.java index 885f824cc..a04b09c77 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/conformance/ProfileUtilities.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/conformance/ProfileUtilities.java @@ -1627,7 +1627,10 @@ public class ProfileUtilities extends TranslatingUtilities { private boolean isMatchingType(StructureDefinition sd, List types) { while (sd != null) { for (TypeRefComponent tr : types) { - if (sd.getType().equals(tr.getCode())) { + if (sd.getUrl().startsWith("http://hl7.org/fhir/StructureDefinition") && sd.getType().equals(tr.getCode())) { + return true; + } + if (sd.getUrl().equals(tr.getCode())) { return true; } } diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/renderers/BundleRenderer.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/renderers/BundleRenderer.java index c8602e48e..937a9034e 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/renderers/BundleRenderer.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/renderers/BundleRenderer.java @@ -7,6 +7,7 @@ import java.util.List; import org.hl7.fhir.exceptions.DefinitionException; import org.hl7.fhir.exceptions.FHIRException; import org.hl7.fhir.exceptions.FHIRFormatError; +import org.hl7.fhir.r5.elementmodel.Element; import org.hl7.fhir.r5.model.Bundle; import org.hl7.fhir.r5.model.Bundle.BundleEntryComponent; import org.hl7.fhir.r5.model.Bundle.BundleEntryRequestComponent; @@ -125,7 +126,7 @@ public class BundleRenderer extends ResourceRenderer { } } - private boolean allEntriesAreHistoryProvenance(List entries) throws UnsupportedEncodingException, FHIRException, IOException { + public static boolean allEntriesAreHistoryProvenance(List entries) throws UnsupportedEncodingException, FHIRException, IOException { for (BaseWrapper be : entries) { if (!"Provenance".equals(be.get("resource").fhirType())) { return false; @@ -134,6 +135,7 @@ public class BundleRenderer extends ResourceRenderer { return !entries.isEmpty(); } + private boolean allEntresAreHistoryProvenance(Bundle b) { for (BundleEntryComponent be : b.getEntry()) { if (!(be.getResource() instanceof Provenance)) { diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/renderers/QuestionnaireResponseRenderer.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/renderers/QuestionnaireResponseRenderer.java index 22f2aee97..4bd77b00c 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/renderers/QuestionnaireResponseRenderer.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/renderers/QuestionnaireResponseRenderer.java @@ -163,8 +163,8 @@ public class QuestionnaireResponseRenderer extends ResourceRenderer { } else { r.setIcon("icon-q-string.png", "Item"); } - String linkId = i.get("linkId").primitiveValue(); - String text = i.get("text").primitiveValue(); + String linkId = i.has("linkId") ? i.get("linkId").primitiveValue() : "??"; + String text = i.has("text") ? i.get("text").primitiveValue() : ""; r.getCells().add(gen.new Cell(null, context.getDefinitionsTarget() == null ? "" : context.getDefinitionsTarget()+"#item."+linkId, linkId, null, null)); r.getCells().add(gen.new Cell(null, null, text, null, null)); r.getCells().add(gen.new Cell(null, null, null, null, null)); diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/IResourceValidator.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/IResourceValidator.java index e62c714c6..7b45a385a 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/IResourceValidator.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/IResourceValidator.java @@ -180,7 +180,14 @@ public interface IResourceValidator { */ public boolean isAllowExamples(); public void setAllowExamples(boolean value) ; - + + /** + * CrumbTrail - whether the validator creates hints to + * @return + */ + public boolean isCrumbTrails(); + public void setCrumbTrails(boolean crumbTrails); + /** * Validate suite 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 2cabc860a..b317e459c 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 @@ -483,6 +483,7 @@ public class I18nConstants { public static final String VALIDATION_VAL_PROFILE_OUTOFORDER = "Validation_VAL_Profile_OutOfOrder"; public static final String VALIDATION_VAL_PROFILE_SLICEORDER = "Validation_VAL_Profile_SliceOrder"; public static final String VALIDATION_VAL_PROFILE_UNKNOWN = "Validation_VAL_Profile_Unknown"; + public static final String VALIDATION_VAL_GLOBAL_PROFILE_UNKNOWN = "VALIDATION_VAL_GLOBAL_PROFILE_UNKNOWN"; public static final String VALIDATION_VAL_PROFILE_WRONGTYPE = "Validation_VAL_Profile_WrongType"; public static final String VALIDATION_VAL_PROFILE_WRONGTYPE2 = "Validation_VAL_Profile_WrongType2"; public static final String VALIDATION_VAL_UNKNOWN_PROFILE = "Validation_VAL_Unknown_Profile"; @@ -517,4 +518,8 @@ public class I18nConstants { public static final String TYPE_CHECKS_PATTERN_CC_US = "TYPE_CHECKS_PATTERN_CC_US"; public static final String TYPE_CHECKS_FIXED_CC = "TYPE_CHECKS_FIXED_CC"; public static final String TYPE_CHECKS_FIXED_CC_US = "TYPE_CHECKS_FIXED_CC_US"; -} \ No newline at end of file + public static final String VALIDATION_VAL_PROFILE_SIGNPOST = "VALIDATION_VAL_PROFILE_SIGNPOST"; + public static final String VALIDATION_VAL_PROFILE_SIGNPOST_META = "VALIDATION_VAL_PROFILE_SIGNPOST_META"; + public static final String VALIDATION_VAL_PROFILE_SIGNPOST_GLOBAL = "VALIDATION_VAL_PROFILE_SIGNPOST_GLOBAL"; + public static final String ERROR_GENERATING_SNAPSHOT = "ERROR_GENERATING_SNAPSHOT"; +} diff --git a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/tests/BaseTestingUtilities.java b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/tests/BaseTestingUtilities.java index a7010a9db..3467c71eb 100644 --- a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/tests/BaseTestingUtilities.java +++ b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/tests/BaseTestingUtilities.java @@ -41,6 +41,7 @@ public class BaseTestingUtilities { } } + public static InputStream loadTestResourceStream(String... paths) throws IOException { String dir = System.getenv("FHIR-TEST-CASES"); if (dir != null && new File(dir).exists()) { diff --git a/org.hl7.fhir.utilities/src/main/resources/Messages.properties b/org.hl7.fhir.utilities/src/main/resources/Messages.properties index 3e030d751..eead73626 100644 --- a/org.hl7.fhir.utilities/src/main/resources/Messages.properties +++ b/org.hl7.fhir.utilities/src/main/resources/Messages.properties @@ -506,7 +506,7 @@ ALL_OK = All OK SEARCHPARAMETER_NOTFOUND = Unable to find the base Search Parameter {0} so can't check that this SearchParameter is a proper derivation from it SEARCHPARAMETER_BASE_WRONG = The base {1} is not listed as a base in the derivedFrom SearchParameter SEARCHPARAMETER_TYPE_WRONG = The type {1} is different to the type {0} in the derivedFrom SearchParameter -SEARCHPARAMETER_TYPE_WRONG = The expression "{1}" is different to the expression "{0}" in the derivedFrom SearchParameter, and this likely indicates that the derivation relationship is not valid +SEARCHPARAMETER_EXP_WRONG = The expression "{2}" is not compatible with the expression "{1}" in the derivedFrom SearchParameter {0}, and this likely indicates that the derivation relationship is not valid VALUESET_NO_SYSTEM_WARNING = No System specified, so Concepts and Filters can't be checked VALUESET_INCLUDE_INVALID_CONCEPT_CODE = The code {1} is not valid in the system {0} VALUESET_INCLUDE_INVALID_CONCEPT_CODE_VER = The code {2} is not valid in the system {0} version {1} @@ -518,3 +518,8 @@ TYPE_CHECKS_PATTERN_CC_US = The pattern [system {0}, code {1}, display "{2}" and TYPE_CHECKS_FIXED_CC = The pattern [system {0}, code {1}, and display "{2}"] defined in the profile {3} not found. Issues: {4} TYPE_CHECKS_FIXED_CC_US = The pattern [system {0}, code {1}, display "{2}" and userSelected {5}] defined in the profile {3} not found. Issues: {4} Internal_error = Internal error: {0} +VALIDATION_VAL_GLOBAL_PROFILE_UNKNOWN = Global Profile reference "{0}" from IG {1} could not be resolved, so has not been checked +VALIDATION_VAL_PROFILE_SIGNPOST = Validate resource against profile {0} +VALIDATION_VAL_PROFILE_SIGNPOST_META = Validate resource against profile {0} - listed in meta +VALIDATION_VAL_PROFILE_SIGNPOST_GLOBAL = Validate resource against profile {0} - a global profile in {1} +ERROR_GENERATING_SNAPSHOT = Error generating Snapshot: {0} (this usually arises from a problem in the differential) diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/BaseValidator.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/BaseValidator.java index ec8df180a..35034d02b 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/BaseValidator.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/BaseValidator.java @@ -346,7 +346,7 @@ public class BaseValidator { * Set this parameter to false if the validation does not pass * @return Returns thePass (in other words, returns true if the rule did not fail validation) */ - protected boolean rule(List errors, IssueType type, String path, boolean thePass, String msg, String html) { + protected boolean ruleHtml(List errors, IssueType type, String path, boolean thePass, String msg, String html) { if (!thePass) { msg = context.formatMessage(msg, null); html = context.formatMessage(html, null); diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ValidationEngine.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ValidationEngine.java index 1823df8f3..4cd25ce0a 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ValidationEngine.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ValidationEngine.java @@ -264,7 +264,9 @@ public class ValidationEngine implements IValidatorResourceFetcher { private boolean assumeValidRestReferences; private boolean noExtensibleBindingMessages; private boolean securityChecks; + private boolean crumbTrails; private Locale locale; + private List igs = new ArrayList<>(); private class AsteriskFilter implements FilenameFilter { String dir; @@ -756,6 +758,7 @@ public class ValidationEngine implements IValidatorResourceFetcher { context.cacheResource(r); if (r instanceof ImplementationGuide) { canonical = ((ImplementationGuide) r).getUrl(); + igs.add((ImplementationGuide) r); if (canonical.contains("/ImplementationGuide/")) { Resource r2 = r.copy(); ((ImplementationGuide) r2).setUrl(canonical.substring(0, canonical.indexOf("/ImplementationGuide/"))); @@ -765,8 +768,9 @@ public class ValidationEngine implements IValidatorResourceFetcher { } } } - if (canonical != null) + if (canonical != null) { grabNatives(source, canonical); + } } public Resource loadFileWithErrorChecking(String version, Entry t, String fn) { @@ -1311,8 +1315,10 @@ public class ValidationEngine implements IValidatorResourceFetcher { validator.setAssumeValidRestReferences(assumeValidRestReferences); validator.setNoExtensibleWarnings(noExtensibleBindingMessages); validator.setSecurityChecks(securityChecks); + validator.setCrumbTrails(crumbTrails); validator.getContext().setLocale(locale); validator.setFetcher(this); + validator.getImplementationGuides().addAll(igs); return validator; } @@ -1758,6 +1764,15 @@ public class ValidationEngine implements IValidatorResourceFetcher { this.securityChecks = securityChecks; } + + public boolean isCrumbTrails() { + return crumbTrails; + } + + public void setCrumbTrails(boolean crumbTrails) { + this.crumbTrails = crumbTrails; + } + public byte[] transformVersion(String source, String targetVer, FhirFormat format, Boolean canDoNative) throws FHIRException, IOException, Exception { Content cnt = loadContent(source, "validate"); org.hl7.fhir.r5.elementmodel.Element src = Manager.parse(context, new ByteArrayInputStream(cnt.focus), cnt.cntType); diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/model/CliContext.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/model/CliContext.java index 89395d687..22fb33dba 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/model/CliContext.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/model/CliContext.java @@ -65,6 +65,9 @@ public class CliContext { @JsonProperty("securityChecks") private boolean securityChecks = false; + @JsonProperty("crumbTrails") + private boolean crumbTrails = false; + @JsonProperty("locale") private String locale = Locale.ENGLISH.getDisplayLanguage(); @@ -425,6 +428,14 @@ public class CliContext { return this; } + public boolean isCrumbTrails() { + return crumbTrails; + } + + public void setCrumbTrails(boolean crumbTrails) { + this.crumbTrails = crumbTrails; + } + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/services/ValidationService.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/services/ValidationService.java index 7b88bfe02..ce124d773 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/services/ValidationService.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/services/ValidationService.java @@ -194,6 +194,7 @@ public class ValidationService { validator.setAssumeValidRestReferences(cliContext.isAssumeValidRestReferences()); validator.setNoExtensibleBindingMessages(cliContext.isNoExtensibleBindingMessages()); validator.setSecurityChecks(cliContext.isSecurityChecks()); + validator.setCrumbTrails(cliContext.isCrumbTrails()); TerminologyCache.setNoCaching(cliContext.isNoInternalCaching()); return validator; } diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/utils/Params.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/utils/Params.java index ddfdf0401..989dabc2b 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/utils/Params.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/utils/Params.java @@ -50,6 +50,7 @@ public class Params { public static final String NO_INTERNAL_CACHING = "-no-internal-caching"; public static final String NO_EXTENSIBLE_BINDING_WARNINGS = "-no-extensible-binding-warnings"; public static final String SECURITY_CHECKS = "-security-checks"; + public static final String CRUMB_TRAIL = "-crumb-trails"; /** * Checks the list of passed in params to see if it contains the passed in param. @@ -153,7 +154,9 @@ public class Params { } else if (args[i].equals(SNAPSHOT)) { cliContext.setMode(Validator.EngineMode.SNAPSHOT); } else if (args[i].equals(SECURITY_CHECKS)) { - cliContext.setSecurityChecks(true); + cliContext.setSecurityChecks(true); + } else if (args[i].equals(CRUMB_TRAIL)) { + cliContext.setCrumbTrails(true); } else if (args[i].equals(SCAN)) { cliContext.setMode(Validator.EngineMode.SCAN); } else if (args[i].equals(TERMINOLOGY)) { 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 99186141f..bf9cece1d 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 @@ -104,6 +104,8 @@ import org.hl7.fhir.r5.model.ExpressionNode; import org.hl7.fhir.r5.model.Extension; import org.hl7.fhir.r5.model.HumanName; import org.hl7.fhir.r5.model.Identifier; +import org.hl7.fhir.r5.model.ImplementationGuide; +import org.hl7.fhir.r5.model.ImplementationGuide.ImplementationGuideGlobalComponent; import org.hl7.fhir.r5.model.InstantType; import org.hl7.fhir.r5.model.IntegerType; import org.hl7.fhir.r5.model.Period; @@ -141,6 +143,7 @@ import org.hl7.fhir.validation.instance.type.CodeSystemValidator; import org.hl7.fhir.validation.instance.type.MeasureValidator; import org.hl7.fhir.validation.instance.type.QuestionnaireValidator; import org.hl7.fhir.validation.instance.type.SearchParameterValidator; +import org.hl7.fhir.validation.instance.type.StructureDefinitionValidator; import org.hl7.fhir.validation.instance.type.ValueSetValidator; import org.hl7.fhir.utilities.CommaSeparatedStringBuilder; import org.hl7.fhir.utilities.Utilities; @@ -328,7 +331,8 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat private BestPracticeWarningLevel bpWarnings; private String validationLanguage; private boolean baseOnly; - + + private List igs = new ArrayList<>(); private List extensionDomains = new ArrayList(); private IdStatus resourceIdRule; @@ -357,6 +361,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat private boolean allowExamples; private boolean securityChecks; private ProfileUtilities profileUtilities; + private boolean crumbTrails; public InstanceValidator(IWorkerContext theContext, IEvaluationContext hostServices) { super(theContext); @@ -445,6 +450,13 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat this.allowExamples = value; } + public boolean isCrumbTrails() { + return crumbTrails; + } + + public void setCrumbTrails(boolean crumbTrails) { + this.crumbTrails = crumbTrails; + } private boolean allowUnknownExtension(String url) { if ((allowExamples && (url.contains("example.org") || url.contains("acme.com"))) || url.contains("nema.org") || url.startsWith("http://hl7.org/fhir/tools/StructureDefinition/") || url.equals("http://hl7.org/fhir/StructureDefinition/structuredefinition-expression")) @@ -2716,6 +2728,10 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat return extensionDomains; } + public List getImplementationGuides() { + return igs; + } + private StructureDefinition getProfileForType(String type, List list) { for (TypeRefComponent tr : list) { String url = tr.getWorkingCode(); @@ -3361,6 +3377,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat resolveBundleReferences(element, new ArrayList()); } startInner(hostContext, errors, resource, element, defn, stack, hostContext.isCheckSpecials()); + hint(errors, IssueType.INFORMATIONAL, element.line(), element.col(), stack.getLiteralPath(), !crumbTrails, I18nConstants.VALIDATION_VAL_PROFILE_SIGNPOST, defn.getUrl()); Element meta = element.getNamedChild(META); if (meta != null) { @@ -3371,6 +3388,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat StructureDefinition sd = context.fetchResource(StructureDefinition.class, profile.primitiveValue()); if (!defn.getUrl().equals(profile.primitiveValue())) { if (warning(errors, IssueType.STRUCTURE, element.line(), element.col(), stack.getLiteralPath() + ".meta.profile[" + i + "]", sd != null, I18nConstants.VALIDATION_VAL_PROFILE_UNKNOWN, profile.primitiveValue())) { + hint(errors, IssueType.INFORMATIONAL, element.line(), element.col(), stack.getLiteralPath(), !crumbTrails, I18nConstants.VALIDATION_VAL_PROFILE_SIGNPOST_META, sd.getUrl()); stack.resetIds(); startInner(hostContext, errors, resource, element, sd, stack, false); } @@ -3378,6 +3396,19 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat i++; } } + String rt = element.fhirType(); + for (ImplementationGuide ig : igs) { + for (ImplementationGuideGlobalComponent gl : ig.getGlobal()) { + if (rt.equals(gl.getType())) { + StructureDefinition sd = context.fetchResource(StructureDefinition.class, gl.getProfile()); + if (warning(errors, IssueType.STRUCTURE, element.line(), element.col(), stack.getLiteralPath(), sd != null, I18nConstants.VALIDATION_VAL_GLOBAL_PROFILE_UNKNOWN, gl.getProfile())) { + hint(errors, IssueType.INFORMATIONAL, element.line(), element.col(), stack.getLiteralPath(), !crumbTrails, I18nConstants.VALIDATION_VAL_PROFILE_SIGNPOST_GLOBAL, sd.getUrl(), ig.getUrl()); + stack.resetIds(); + startInner(hostContext, errors, resource, element, sd, stack, false); + } + } + } + } } private void resolveBundleReferences(Element element, List bundles) { @@ -3481,7 +3512,9 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat } else if (element.getType().equals("CodeSystem")) { new CodeSystemValidator(context, timeTracker).validateCodeSystem(errors, element, stack); } else if (element.getType().equals("SearchParameter")) { - new SearchParameterValidator(context, timeTracker).validateSearchParameter(errors, element, stack); + new SearchParameterValidator(context, timeTracker, fpe).validateSearchParameter(errors, element, stack); + } else if (element.getType().equals("StructureDefinition")) { + new StructureDefinitionValidator(context, timeTracker, fpe).validateStructureDefinition(errors, element, stack); } else if (element.getType().equals("ValueSet")) { new ValueSetValidator(context, timeTracker).validateValueSet(errors, element, stack); } diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/type/BundleValidator.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/type/BundleValidator.java index a46de6267..a2f7760bd 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/type/BundleValidator.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/type/BundleValidator.java @@ -192,7 +192,7 @@ public class BundleValidator extends BaseValidator{ boolean ok = bundle.hasChild(META) && bundle.getNamedChild(META).hasChild(LAST_UPDATED) && bundle.getNamedChild(META).getNamedChild(LAST_UPDATED).hasValue(); - rule(errors, IssueType.REQUIRED, stack.getLiteralPath(), ok, I18nConstants.DOCUMENT_DATE_REQUIRED, I18nConstants.DOCUMENT_DATE_REQUIRED_HTML); + ruleHtml(errors, IssueType.REQUIRED, stack.getLiteralPath(), ok, I18nConstants.DOCUMENT_DATE_REQUIRED, I18nConstants.DOCUMENT_DATE_REQUIRED_HTML); } private void checkAllInterlinked(List errors, List entries, NodeStack stack, Element bundle, boolean isError) { diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/type/SearchParameterValidator.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/type/SearchParameterValidator.java index 129bf5f3d..f3045da3d 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/type/SearchParameterValidator.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/type/SearchParameterValidator.java @@ -1,12 +1,19 @@ package org.hl7.fhir.validation.instance.type; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; import java.util.List; 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.ExpressionNode; +import org.hl7.fhir.r5.model.ExpressionNode.Kind; +import org.hl7.fhir.r5.model.ExpressionNode.Operation; import org.hl7.fhir.r5.model.SearchParameter; import org.hl7.fhir.r5.model.ValueSet; +import org.hl7.fhir.r5.utils.FHIRPathEngine; import org.hl7.fhir.utilities.Utilities; import org.hl7.fhir.utilities.i18n.I18nConstants; import org.hl7.fhir.utilities.validation.ValidationMessage; @@ -14,13 +21,26 @@ import org.hl7.fhir.utilities.validation.ValidationMessage.IssueType; import org.hl7.fhir.utilities.validation.ValidationMessage.Source; import org.hl7.fhir.validation.BaseValidator; import org.hl7.fhir.validation.TimeTracker; +import org.hl7.fhir.validation.instance.type.SearchParameterValidator.FhirPathSorter; import org.hl7.fhir.validation.instance.utils.NodeStack; public class SearchParameterValidator extends BaseValidator { - public SearchParameterValidator(IWorkerContext context, TimeTracker timeTracker) { + public class FhirPathSorter implements Comparator { + + @Override + public int compare(ExpressionNode arg0, ExpressionNode arg1) { + return arg0.toString().compareTo(arg1.toString()); + } + + } + + private FHIRPathEngine fpe; + + public SearchParameterValidator(IWorkerContext context, TimeTracker timeTracker, FHIRPathEngine fpe) { super(context); source = Source.InstanceValidator; + this.fpe = fpe; this.timeTracker = timeTracker; } @@ -37,10 +57,49 @@ public class SearchParameterValidator extends BaseValidator { rule(errors, IssueType.BUSINESSRULE,stack.getLiteralPath(), sp.hasBase(b.primitiveValue()), I18nConstants.SEARCHPARAMETER_BASE_WRONG, master, b.primitiveValue()); } rule(errors, IssueType.BUSINESSRULE,stack.getLiteralPath(), !cs.hasChild("type") || sp.getType().toCode().equals(cs.getNamedChildValue("type")), I18nConstants.SEARCHPARAMETER_TYPE_WRONG, master, sp.getType().toCode(), cs.getNamedChildValue("type")); - warning(errors, IssueType.BUSINESSRULE,stack.getLiteralPath(), !cs.hasChild("expression") || sp.getExpression().equals(cs.getNamedChildValue("expression")), I18nConstants.SEARCHPARAMETER_EXP_WRONG, master, sp.getExpression(), cs.getNamedChildValue("expression")); - // todo: cjeck compositions + if (cs.hasChild("expression") && !sp.getExpression().equals(cs.getNamedChildValue("expression"))) { + List bases = new ArrayList<>(); + for (Element b : cs.getChildren("base")) { + bases.add(b.primitiveValue()); + } + String expThis = canonicalise(cs.getNamedChildValue("expression"), bases); + String expOther = canonicalise(sp.getExpression(), bases); + warning(errors, IssueType.BUSINESSRULE,stack.getLiteralPath(), expThis.equals(expOther), I18nConstants.SEARCHPARAMETER_EXP_WRONG, master, sp.getExpression(), cs.getNamedChildValue("expression")); + } + // todo: check compositions } } } + private String canonicalise(String path, List bases) { + ExpressionNode exp = fpe.parse(path); + List pass = new ArrayList<>(); + while (exp != null) { + if ((exp.getKind() != Kind.Name && !(exp.getKind() == Kind.Group && exp.getGroup().getKind() == Kind.Name))) { + return path; + } + if (exp.getOperation() != null && exp.getOperation() != Operation.Union) { + return path; + } + ExpressionNode nexp = exp.getOpNext(); + exp.setOperation(null); + exp.setOpNext(null); + String name = exp.getKind() == Kind.Name ? exp.getName() : exp.getGroup().getName(); + if (context.getResourceNames().contains(name)) { + if (bases.contains(name)) { + pass.add(exp); + } + } else { + pass.add(exp); + } + exp = nexp; + } + Collections.sort(pass, new FhirPathSorter()); + for (int i = 0; i < pass.size()-1; i++) { + pass.get(i).setOperation(Operation.Union); + pass.get(i).setOpNext(pass.get(i+1)); + } + return pass.get(0).toString(); + } + } diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/type/StructureDefinitionValidator.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/type/StructureDefinitionValidator.java new file mode 100644 index 000000000..9f9746491 --- /dev/null +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/type/StructureDefinitionValidator.java @@ -0,0 +1,115 @@ +package org.hl7.fhir.validation.instance.type; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +import org.hl7.fhir.r5.model.StructureDefinition; +import org.hl7.fhir.convertors.VersionConvertor_10_50; +import org.hl7.fhir.convertors.VersionConvertor_14_50; +import org.hl7.fhir.convertors.VersionConvertor_30_50; +import org.hl7.fhir.convertors.VersionConvertor_40_50; +import org.hl7.fhir.exceptions.FHIRException; +import org.hl7.fhir.r5.conformance.ProfileUtilities; +import org.hl7.fhir.r5.conformance.ProfileUtilities.ProfileKnowledgeProvider; +import org.hl7.fhir.r5.context.IWorkerContext; +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.ElementDefinition; +import org.hl7.fhir.r5.model.ExpressionNode; +import org.hl7.fhir.r5.model.ExpressionNode.Kind; +import org.hl7.fhir.r5.model.ExpressionNode.Operation; +import org.hl7.fhir.r5.model.SearchParameter; +import org.hl7.fhir.r5.model.ValueSet; +import org.hl7.fhir.r5.utils.FHIRPathEngine; +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.validation.BaseValidator; +import org.hl7.fhir.validation.TimeTracker; +import org.hl7.fhir.validation.instance.type.StructureDefinitionValidator.FhirPathSorter; +import org.hl7.fhir.validation.instance.utils.NodeStack; + +public class StructureDefinitionValidator extends BaseValidator { + + public class FhirPathSorter implements Comparator { + + @Override + public int compare(ExpressionNode arg0, ExpressionNode arg1) { + return arg0.toString().compareTo(arg1.toString()); + } + + } + + private FHIRPathEngine fpe; + + public StructureDefinitionValidator(IWorkerContext context, TimeTracker timeTracker, FHIRPathEngine fpe) { + super(context); + source = Source.InstanceValidator; + this.fpe = fpe; + this.timeTracker = timeTracker; + } + + public void validateStructureDefinition(List errors, Element src, NodeStack stack) { + StructureDefinition sd; + try { + sd = loadAsSD(src); + List snapshot = sd.getSnapshot().getElement(); + sd.setSnapshot(null); + StructureDefinition base = context.fetchResource(StructureDefinition.class, sd.getBaseDefinition()); + if (warning(errors, IssueType.NOTFOUND, stack.getLiteralPath(), base != null, I18nConstants.UNABLE_TO_FIND_BASE__FOR_, sd.getBaseDefinition(), "StructureDefinition, so can't check the differential")) { + List msgs = new ArrayList<>(); + ProfileUtilities pu = new ProfileUtilities(context, msgs, null); + pu.generateSnapshot(base, sd, sd.getUrl(), "http://hl7.org/fhir", sd.getName()); + if (msgs.size() > 0) { + for (ValidationMessage msg : msgs) { + // we need to set the location for the context + String loc = msg.getLocation(); + if (loc.contains("#")) { + msg.setLocation(stack.getLiteralPath()+".differential.element.where(path = '"+loc.substring(loc.indexOf("#")+1)+"')"); + } else { + msg.setLocation(stack.getLiteralPath()); + } + errors.add(msg); + } + } + } + if (!snapshot.isEmpty()) { + System.out.print("?"); + } + } catch (FHIRException | IOException e) { + rule(errors, IssueType.EXCEPTION, stack.getLiteralPath(), false, I18nConstants.ERROR_GENERATING_SNAPSHOT, e.getMessage()); + } + } + + private StructureDefinition loadAsSD(Element src) throws FHIRException, IOException { + ByteArrayOutputStream bs = new ByteArrayOutputStream(); + Manager.compose(context, src, bs, FhirFormat.JSON, OutputStyle.NORMAL, null); + if (VersionUtilities.isR2Ver(context.getVersion())) { + org.hl7.fhir.dstu2.model.Resource r2 = new org.hl7.fhir.dstu2.formats.JsonParser().parse(bs.toByteArray()); + return (StructureDefinition) VersionConvertor_10_50.convertResource(r2); + } + if (VersionUtilities.isR2BVer(context.getVersion())) { + org.hl7.fhir.dstu2016may.model.Resource r2b = new org.hl7.fhir.dstu2016may.formats.JsonParser().parse(bs.toByteArray()); + return (StructureDefinition) VersionConvertor_14_50.convertResource(r2b); + } + if (VersionUtilities.isR3Ver(context.getVersion())) { + org.hl7.fhir.dstu3.model.Resource r3 = new org.hl7.fhir.dstu3.formats.JsonParser().parse(bs.toByteArray()); + return (StructureDefinition) VersionConvertor_30_50.convertResource(r3, false); + } + if (VersionUtilities.isR4Ver(context.getVersion())) { + org.hl7.fhir.r4.model.Resource r4 = new org.hl7.fhir.r4.formats.JsonParser().parse(bs.toByteArray()); + return (StructureDefinition) VersionConvertor_40_50.convertResource(r4); + } + return (StructureDefinition) new org.hl7.fhir.r5.formats.JsonParser().parse(bs.toByteArray()); + } + +} diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/profile/ProfileValidator.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/profile/ProfileValidator.java index 221963885..a804b5243 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/profile/ProfileValidator.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/profile/ProfileValidator.java @@ -72,7 +72,7 @@ public class ProfileValidator extends BaseValidator { protected boolean rule(List errors, IssueType type, String path, boolean b, String msg) { String rn = path.contains(".") ? path.substring(0, path.indexOf(".")) : path; - return super.rule(errors, type, path, b, msg, ""+rn+": "+Utilities.escapeXml(msg)); + return super.ruleHtml(errors, type, path, b, msg, ""+rn+": "+Utilities.escapeXml(msg)); } public List validate(StructureDefinition profile, boolean forBuild) { diff --git a/org.hl7.fhir.validation/src/test/java/org/hl7/fhir/validation/tests/ValidationTestSuite.java b/org.hl7.fhir.validation/src/test/java/org/hl7/fhir/validation/tests/ValidationTestSuite.java index 01f865e07..8cb0ee057 100644 --- a/org.hl7.fhir.validation/src/test/java/org/hl7/fhir/validation/tests/ValidationTestSuite.java +++ b/org.hl7.fhir.validation/src/test/java/org/hl7/fhir/validation/tests/ValidationTestSuite.java @@ -26,6 +26,7 @@ import org.hl7.fhir.r5.model.Base; import org.hl7.fhir.r5.model.CanonicalResource; import org.hl7.fhir.r5.model.Constants; import org.hl7.fhir.r5.model.FhirPublication; +import org.hl7.fhir.r5.model.ImplementationGuide; import org.hl7.fhir.r5.model.Patient; import org.hl7.fhir.r5.model.Resource; import org.hl7.fhir.r5.model.StructureDefinition; @@ -169,12 +170,18 @@ public class ValidationTestSuite implements IEvaluationContext, IValidatorResour vCurr.loadIg(e.getAsString(), true); } } + if (content.has("crumb-trail")) { + val.setCrumbTrails(content.get("crumb-trail").getAsBoolean()); + } if (content.has("supporting")) { for (JsonElement e : content.getAsJsonArray("supporting")) { String filename = e.getAsString(); String contents = TestingUtilities.loadTestResource("validator", filename); CanonicalResource mr = (CanonicalResource) loadResource(filename, contents); val.getContext().cacheResource(mr); + if (mr instanceof ImplementationGuide) { + val.getImplementationGuides().add((ImplementationGuide) mr); + } } } if (content.has("profiles")) { @@ -219,6 +226,9 @@ public class ValidationTestSuite implements IEvaluationContext, IValidatorResour String contents = TestingUtilities.loadTestResource("validator", filename); CanonicalResource mr = (CanonicalResource) loadResource(filename, contents); val.getContext().cacheResource(mr); + if (mr instanceof ImplementationGuide) { + val.getImplementationGuides().add((ImplementationGuide) mr); + } } } String filename = profile.get("source").getAsString(); diff --git a/pom.xml b/pom.xml index d4987489e..29298df4b 100644 --- a/pom.xml +++ b/pom.xml @@ -17,7 +17,7 @@ 5.0.0 - 1.1.22 + 1.1.23 5.6.2 3.0.0-M4 0.8.5