From 95306c3d64eafbf7cfc38cca0c45147b387e30f0 Mon Sep 17 00:00:00 2001 From: James Agnew Date: Wed, 7 Jul 2021 16:28:41 -0400 Subject: [PATCH] Add NPM validation support module (#2782) * Add NPM validation support module * Add changelog --- .../java/ca/uhn/fhir/util/ClasspathUtil.java | 17 +++-- .../uhn/hapi/fhir/docs/ValidatorExamples.java | 44 +++++++++++- ...782-add-npm-validation-support-module.yaml | 5 ++ .../docs/validation/instance_validator.md | 17 +++++ .../validation/validation_support_modules.md | 28 +++++--- .../support/NpmPackageValidationSupport.java | 55 ++++++++++++++ .../PrePopulatedValidationSupport.java | 39 +++++++--- .../PrePopulatedValidationSupportTest.java | 36 ++++++++++ .../NpmPackageValidationSupportTest.java | 67 ++++++++++++++++++ .../resources/package/UK.Core.r4-1.1.0.tgz | Bin 0 -> 28433 bytes 10 files changed, 283 insertions(+), 25 deletions(-) create mode 100644 hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_5_0/2782-add-npm-validation-support-module.yaml create mode 100644 hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/NpmPackageValidationSupport.java create mode 100644 hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/support/PrePopulatedValidationSupportTest.java create mode 100644 hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/NpmPackageValidationSupportTest.java create mode 100644 hapi-fhir-validation/src/test/resources/package/UK.Core.r4-1.1.0.tgz diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ClasspathUtil.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ClasspathUtil.java index 6c1def2fdef..14d8e3dcc57 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ClasspathUtil.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ClasspathUtil.java @@ -56,18 +56,25 @@ public class ClasspathUtil { /** * Load a classpath resource, throw an {@link InternalErrorException} if not found + * + * @throws InternalErrorException If the resource can't be found */ @Nonnull public static InputStream loadResourceAsStream(String theClasspath) { - InputStream retVal = ClasspathUtil.class.getResourceAsStream(theClasspath); + String classpath = theClasspath; + if (classpath.startsWith("classpath:")) { + classpath = classpath.substring("classpath:".length()); + } + + InputStream retVal = ClasspathUtil.class.getResourceAsStream(classpath); if (retVal == null) { - if (theClasspath.startsWith("/")) { - retVal = ClasspathUtil.class.getResourceAsStream(theClasspath.substring(1)); + if (classpath.startsWith("/")) { + retVal = ClasspathUtil.class.getResourceAsStream(classpath.substring(1)); } else { - retVal = ClasspathUtil.class.getResourceAsStream("/" + theClasspath); + retVal = ClasspathUtil.class.getResourceAsStream("/" + classpath); } if (retVal == null) { - throw new InternalErrorException("Unable to find classpath resource: " + theClasspath); + throw new InternalErrorException("Unable to find classpath resource: " + classpath); } } return retVal; diff --git a/hapi-fhir-docs/src/main/java/ca/uhn/hapi/fhir/docs/ValidatorExamples.java b/hapi-fhir-docs/src/main/java/ca/uhn/hapi/fhir/docs/ValidatorExamples.java index 2b4eef04aad..4c12593024c 100644 --- a/hapi-fhir-docs/src/main/java/ca/uhn/hapi/fhir/docs/ValidatorExamples.java +++ b/hapi-fhir-docs/src/main/java/ca/uhn/hapi/fhir/docs/ValidatorExamples.java @@ -40,8 +40,10 @@ import org.apache.commons.io.filefilter.WildcardFileFilter; import org.hl7.fhir.common.hapi.validation.support.CachingValidationSupport; import org.hl7.fhir.common.hapi.validation.support.CommonCodeSystemsTerminologyService; import org.hl7.fhir.common.hapi.validation.support.InMemoryTerminologyServerValidationSupport; +import org.hl7.fhir.common.hapi.validation.support.NpmPackageValidationSupport; import org.hl7.fhir.common.hapi.validation.support.PrePopulatedValidationSupport; import org.hl7.fhir.common.hapi.validation.support.RemoteTerminologyServiceValidationSupport; +import org.hl7.fhir.common.hapi.validation.support.SnapshotGeneratingValidationSupport; import org.hl7.fhir.common.hapi.validation.support.ValidationSupportChain; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.common.hapi.validation.validator.FhirInstanceValidator; @@ -437,5 +439,45 @@ public class ValidatorExamples { // END SNIPPET: validateFiles } - + + + @SuppressWarnings("unused") + private static void npm() throws Exception { + // START SNIPPET: npm + // Create an NPM Package Support module and load one package in from + // the classpath + FhirContext ctx = FhirContext.forR4(); + NpmPackageValidationSupport npmPackageSupport = new NpmPackageValidationSupport(ctx); + npmPackageSupport.loadPackageFromClasspath("classpath:package/UK.Core.r4-1.1.0.tgz"); + + // Create a support chain including the NPM Package Support + ValidationSupportChain validationSupportChain = new ValidationSupportChain( + npmPackageSupport, + new DefaultProfileValidationSupport(ctx), + new CommonCodeSystemsTerminologyService(ctx), + new InMemoryTerminologyServerValidationSupport(ctx), + new SnapshotGeneratingValidationSupport(ctx) + ); + CachingValidationSupport validationSupport = new CachingValidationSupport(validationSupportChain); + + // Create a validator. Note that for good performance you can create as many validator objects + // as you like, but you should reuse the same validation support object in all of the,. + FhirValidator validator = ctx.newValidator(); + FhirInstanceValidator instanceValidator = new FhirInstanceValidator(validationSupport); + validator.registerValidatorModule(instanceValidator); + + // Create a test patient to validate + Patient patient = new Patient(); + patient.getMeta().addProfile("https://fhir.nhs.uk/R4/StructureDefinition/UKCore-Patient"); + // System but not value set for NHS identifier (this should generate an error) + patient.addIdentifier().setSystem("https://fhir.nhs.uk/Id/nhs-number"); + + // Perform the validation + ValidationResult outcome = validator.validateWithResult(patient); + // END SNIPPET: npm + } + + + + } diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_5_0/2782-add-npm-validation-support-module.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_5_0/2782-add-npm-validation-support-module.yaml new file mode 100644 index 00000000000..3bbebbe094b --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_5_0/2782-add-npm-validation-support-module.yaml @@ -0,0 +1,5 @@ +--- +type: add +issue: 2782 +title: "A new Validation Support Module has been added that can use NPM Packages to supply + validation conformance artifacts programatcally to the validator." diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/validation/instance_validator.md b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/validation/instance_validator.md index 3939318f427..ec17ebfdf9e 100644 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/validation/instance_validator.md +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/validation/instance_validator.md @@ -59,6 +59,23 @@ to validate the resource. It will not work unless you include the {{snippet:classpath:/ca/uhn/hapi/fhir/docs/ValidatorExamples.java|instanceValidator}} ``` + + +# Validating Using Packages + +HAPI FHIR supports the use of FHIR NPM Packages for supplying validation artifacts. + +When using the HAPI FHIR [JPA Server](../server_jpa/) you can simply upload your packages into the JPA Server package registry and the contents will be made available to the validator. + +If you are using the validator as a standalone service (i.e. you are invoking it via a Java call) you will need to explcitly make your packages available to the validation support chain. + +The following example shows the use of [NpmPackageValidationSupport](./validation_support_modules.html#npmpackagevalidationsupport) to load a package and use it to validate a resource. + +```java +{{snippet:classpath:/ca/uhn/hapi/fhir/docs/ValidatorExamples.java|npm}} +``` + + # Migrating to HAPI FHIR 5.x diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/validation/validation_support_modules.md b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/validation/validation_support_modules.md index 5355fe98074..05da89a6993 100644 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/validation/validation_support_modules.md +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/validation/validation_support_modules.md @@ -12,43 +12,53 @@ There are a several implementations of the [IValidationSupport](/hapi-fhir/apido # ValidationSupportChain -[JavaDoc](/hapi-fhir/apidocs/hapi-fhir-validation/org/hl7/fhir/common/hapi/validation/support/ValidationSupportChain.html) / [Source](https://github.com/hapifhir/hapi-fhir/blob/ja_20200218_validation_api_changes/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/ValidationSupportChain.java) +[JavaDoc](/hapi-fhir/apidocs/hapi-fhir-validation/org/hl7/fhir/common/hapi/validation/support/ValidationSupportChain.html) / [Source](https://github.com/hapifhir/hapi-fhir/blob/master/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/ValidationSupportChain.java) This module can be used to combine multiple implementations together so that for every request, each support class instance in the chain is tried in sequence. Note that nearly all methods in the [IValidationSupport](/hapi-fhir/apidocs/hapi-fhir-base/ca/uhn/fhir/context/support/IValidationSupport.html) interface are permitted to return `null` if they are not able to service a particular method call. So for example, if a call to the [`validateCode`](/hapi-fhir/apidocs/hapi-fhir-base/ca/uhn/fhir/context/support/IValidationSupport.html#validateCode(ca.uhn.fhir.context.support.ValidationSupportContext,ca.uhn.fhir.context.support.ConceptValidationOptions,java.lang.String,java.lang.String,java.lang.String,java.lang.String)) method is made, the validator will try each module in the chain until one of them returns a non-null response. # DefaultProfileValidationSupport -[JavaDoc](/hapi-fhir/apidocs/hapi-fhir-base/undefined/ca/uhn/fhir/context/support/DefaultProfileValidationSupport.html) / [Source](https://github.com/hapifhir/hapi-fhir/blob/ja_20200218_validation_api_changes/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/support/DefaultProfileValidationSupport.java) +[JavaDoc](/hapi-fhir/apidocs/hapi-fhir-base/undefined/ca/uhn/fhir/context/support/DefaultProfileValidationSupport.html) / [Source](https://github.com/hapifhir/hapi-fhir/blob/master/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/support/DefaultProfileValidationSupport.java) This module supplies the built-in FHIR core structure definitions, including both FHIR resource definitions (StructureDefinition resources) and FHIR built-in vocabulary (ValueSet and CodeSystem resources). # InMemoryTerminologyServerValidationSupport -[JavaDoc](/hapi-fhir/apidocs/hapi-fhir-validation/org/hl7/fhir/common/hapi/validation/support/InMemoryTerminologyServerValidationSupport.html) / [Source](https://github.com/jamesagnew/hapi-fhir/blob/ja_20200218_validation_api_changes/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/InMemoryTerminologyServerValidationSupport.java) +[JavaDoc](/hapi-fhir/apidocs/hapi-fhir-validation/org/hl7/fhir/common/hapi/validation/support/InMemoryTerminologyServerValidationSupport.html) / [Source](https://github.com/jamesagnew/hapi-fhir/blob/master/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/InMemoryTerminologyServerValidationSupport.java) This module acts as a simple terminology service that can validate codes against ValueSet and CodeSystem resources purely in-memory (i.e. with no database). This is sufficient in many basic cases, although it is not able to validate CodeSystems with external content (i.e CodeSystems where the `CodeSystem.content` field is `external`, such as the LOINC and SNOMED CT CodeSystems). # PrePopulatedValidationSupport -[JavaDoc](/hapi-fhir/apidocs/hapi-fhir-validation/org/hl7/fhir/common/hapi/validation/support/PrePopulatedValidationSupport.html) / [Source](https://github.com/jamesagnew/hapi-fhir/blob/ja_20200218_validation_api_changes/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/PrePopulatedValidationSupport.java) +[JavaDoc](/hapi-fhir/apidocs/hapi-fhir-validation/org/hl7/fhir/common/hapi/validation/support/PrePopulatedValidationSupport.html) / [Source](https://github.com/jamesagnew/hapi-fhir/blob/master/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/PrePopulatedValidationSupport.java) This module contains a series of HashMaps that store loaded conformance resources in memory. Typically this is initialized at startup in order to add custom conformance resources into the chain. + + + +# NpmPackageValidationSupport + +[JavaDoc](/hapi-fhir/apidocs/hapi-fhir-validation/org/hl7/fhir/common/hapi/validation/support/NpmPackageValidationSupport.html) / [Source](https://github.com/jamesagnew/hapi-fhir/blob/master/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/NpmPackageValidationSupport.java) + +This module can be used to load FHIR NPM Packages and supply the conformance resources within them to the validator. See [Validating Using Packages](./instance_validator.html#packages) for am example of how to use this module. + + # CachingValidationSupport -[JavaDoc](/hapi-fhir/apidocs/hapi-fhir-validation/org/hl7/fhir/common/hapi/validation/support/CachingValidationSupport.html) / [Source](https://github.com/jamesagnew/hapi-fhir/blob/ja_20200218_validation_api_changes/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/CachingValidationSupport.java) +[JavaDoc](/hapi-fhir/apidocs/hapi-fhir-validation/org/hl7/fhir/common/hapi/validation/support/CachingValidationSupport.html) / [Source](https://github.com/jamesagnew/hapi-fhir/blob/master/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/CachingValidationSupport.java) This module caches results of calls to a wrapped service implementation for a period of time. This class can be a significant help in terms of performance if you are loading conformance resources or performing terminology operations from a database or disk, but it also has value even for purely in-memory validation since validating codes against a ValueSet can require the expansion of that ValueSet. # SnapshotGeneratingValidationSupport -[JavaDoc](/hapi-fhir/apidocs/hapi-fhir-validation/org/hl7/fhir/common/hapi/validation/support/SnapshotGeneratingValidationSupport.html) / [Source](https://github.com/jamesagnew/hapi-fhir/blob/ja_20200218_validation_api_changes/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/SnapshotGeneratingValidationSupport.java) +[JavaDoc](/hapi-fhir/apidocs/hapi-fhir-validation/org/hl7/fhir/common/hapi/validation/support/SnapshotGeneratingValidationSupport.html) / [Source](https://github.com/jamesagnew/hapi-fhir/blob/master/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/SnapshotGeneratingValidationSupport.java) This module generates StructureDefinition snapshots as needed. This should be added to your chain if you are working wiith differential StructureDefinitions that do not include the snapshot view. # CommonCodeSystemsTerminologyService -[JavaDoc](/hapi-fhir/apidocs/hapi-fhir-validation/org/hl7/fhir/common/hapi/validation/support/CommonCodeSystemsTerminologyService.html) / [Source](https://github.com/jamesagnew/hapi-fhir/blob/ja_20200218_validation_api_changes/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/CommonCodeSystemsTerminologyService.java) +[JavaDoc](/hapi-fhir/apidocs/hapi-fhir-validation/org/hl7/fhir/common/hapi/validation/support/CommonCodeSystemsTerminologyService.html) / [Source](https://github.com/jamesagnew/hapi-fhir/blob/master/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/CommonCodeSystemsTerminologyService.java) This module validates codes in CodeSystems that are not distributed with the FHIR specification because they are difficult to distribute but are commonly used in FHIR resources. @@ -132,7 +142,7 @@ The following table lists vocabulary that is validated by this module: # RemoteTerminologyServiceValidationSupport -[JavaDoc](/hapi-fhir/apidocs/hapi-fhir-validation/org/hl7/fhir/common/hapi/validation/support/RemoteTerminologyServiceValidationSupport.html) / [Source](https://github.com/jamesagnew/hapi-fhir/blob/ja_20200218_validation_api_changes/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/RemoteTerminologyServiceValidationSupport.java) +[JavaDoc](/hapi-fhir/apidocs/hapi-fhir-validation/org/hl7/fhir/common/hapi/validation/support/RemoteTerminologyServiceValidationSupport.html) / [Source](https://github.com/jamesagnew/hapi-fhir/blob/master/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/RemoteTerminologyServiceValidationSupport.java) This module validates codes using a remote FHIR-based terminology server. @@ -145,7 +155,7 @@ This module will invoke the following operations on the remote terminology serve # UnknownCodeSystemWarningValidationSupport -[JavaDoc](/hapi-fhir/apidocs/hapi-fhir-validation/org/hl7/fhir/common/hapi/validation/support/UnknownCodeSystemWarningValidationSupport.html) / [Source](https://github.com/jamesagnew/hapi-fhir/blob/ja_20200218_validation_api_changes/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/UnknownCodeSystemWarningValidationSupport.java) +[JavaDoc](/hapi-fhir/apidocs/hapi-fhir-validation/org/hl7/fhir/common/hapi/validation/support/UnknownCodeSystemWarningValidationSupport.html) / [Source](https://github.com/jamesagnew/hapi-fhir/blob/master/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/UnknownCodeSystemWarningValidationSupport.java) This validation support module may be placed at the end of a ValidationSupportChain in order to configure the validator to generate a warning if a resource being validated contains an unknown code system. diff --git a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/NpmPackageValidationSupport.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/NpmPackageValidationSupport.java new file mode 100644 index 00000000000..4db388eab2c --- /dev/null +++ b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/NpmPackageValidationSupport.java @@ -0,0 +1,55 @@ +package org.hl7.fhir.common.hapi.validation.support; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; +import ca.uhn.fhir.util.ClasspathUtil; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.r4.model.ValueSet; +import org.hl7.fhir.utilities.npm.NpmPackage; + +import javax.annotation.Nonnull; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.Locale; + +/** + * This interceptor loads and parses FHIR NPM Conformance Packages, and makes the + * artifacts foudn within them available to the FHIR validator. + * + * @since 5.5.0 + */ +public class NpmPackageValidationSupport extends PrePopulatedValidationSupport { + + /** + * Constructor + */ + public NpmPackageValidationSupport(@Nonnull FhirContext theFhirContext) { + super(theFhirContext); + } + + /** + * Load an NPM package using a classpath specification, e.g. /path/to/resource/my_package.tgz. The + * classpath spec can optionally be prefixed with the string classpath: + * + * @throws InternalErrorException If the classpath file can't be found + */ + public void loadPackageFromClasspath(String theClasspath) throws IOException { + try (InputStream is = ClasspathUtil.loadResourceAsStream(theClasspath)) { + NpmPackage pkg = NpmPackage.fromPackage(is); + if (pkg.getFolders().containsKey("package")) { + NpmPackage.NpmPackageFolder packageFolder = pkg.getFolders().get("package"); + + for (String nextFile : packageFolder.listFiles()) { + if (nextFile.toLowerCase(Locale.US).endsWith(".json")) { + String input = new String(packageFolder.getContent().get(nextFile), StandardCharsets.UTF_8); + IBaseResource resource = getFhirContext().newJsonParser().parseResource(input); + super.addResource(resource); + } + } + + } + } + } + +} diff --git a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/PrePopulatedValidationSupport.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/PrePopulatedValidationSupport.java index f74dca0f658..4941b497dc1 100644 --- a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/PrePopulatedValidationSupport.java +++ b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/PrePopulatedValidationSupport.java @@ -12,6 +12,7 @@ import org.hl7.fhir.r4.model.CodeSystem; import org.hl7.fhir.r4.model.StructureDefinition; import org.hl7.fhir.r4.model.ValueSet; +import javax.annotation.Nonnull; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -26,7 +27,6 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank; */ public class PrePopulatedValidationSupport extends BaseStaticResourceValidationSupport implements IValidationSupport { - private final FhirContext myFhirContext; private final Map myCodeSystems; private final Map myStructureDefinitions; private final Map myValueSets; @@ -55,7 +55,6 @@ public class PrePopulatedValidationSupport extends BaseStaticResourceValidationS Validate.notNull(theStructureDefinitions, "theStructureDefinitions must not be null"); Validate.notNull(theValueSets, "theValueSets must not be null"); Validate.notNull(theCodeSystems, "theCodeSystems must not be null"); - myFhirContext = theFhirContext; myStructureDefinitions = theStructureDefinitions; myValueSets = theValueSets; myCodeSystems = theCodeSystems; @@ -82,7 +81,7 @@ public class PrePopulatedValidationSupport extends BaseStaticResourceValidationS private String processResourceAndReturnUrl(IBaseResource theCodeSystem, String theResourceName) { Validate.notNull(theCodeSystem, "the" + theResourceName + " must not be null"); - RuntimeResourceDefinition resourceDef = myFhirContext.getResourceDefinition(theCodeSystem); + RuntimeResourceDefinition resourceDef = getFhirContext().getResourceDefinition(theCodeSystem); String actualResourceName = resourceDef.getName(); Validate.isTrue(actualResourceName.equals(theResourceName), "the" + theResourceName + " must be a " + theResourceName + " - Got: " + actualResourceName); @@ -113,16 +112,16 @@ public class PrePopulatedValidationSupport extends BaseStaticResourceValidationS addToMap(theStructureDefinition, myStructureDefinitions, url); } - private void addToMap(T theStructureDefinition, Map map, String theUrl) { + private void addToMap(T theResource, Map theMap, String theUrl) { if (isNotBlank(theUrl)) { - map.put(theUrl, theStructureDefinition); + theMap.put(theUrl, theResource); int lastSlashIdx = theUrl.lastIndexOf('/'); if (lastSlashIdx != -1) { - map.put(theUrl.substring(lastSlashIdx + 1), theStructureDefinition); + theMap.put(theUrl.substring(lastSlashIdx + 1), theResource); int previousSlashIdx = theUrl.lastIndexOf('/', lastSlashIdx - 1); if (previousSlashIdx != -1) { - map.put(theUrl.substring(previousSlashIdx + 1), theStructureDefinition); + theMap.put(theUrl.substring(previousSlashIdx + 1), theResource); } } @@ -143,12 +142,33 @@ public class PrePopulatedValidationSupport extends BaseStaticResourceValidationS * *

*/ - public void addValueSet(ValueSet theValueSet) { + public void addValueSet(IBaseResource theValueSet) { String url = processResourceAndReturnUrl(theValueSet, "ValueSet"); addToMap(theValueSet, myValueSets, url); } + /** + * @param theResource The resource. This method delegates to the type-specific methods (e.g. {@link #addCodeSystem(IBaseResource)}) + * and will do nothing if the resource type is not supported by this class. + * @since 5.5.0 + */ + public void addResource(@Nonnull IBaseResource theResource) { + Validate.notNull(theResource, "theResource must not be null"); + + switch (getFhirContext().getResourceType(theResource)) { + case "StructureDefinition": + addStructureDefinition(theResource); + break; + case "CodeSystem": + addCodeSystem(theResource); + break; + case "ValueSet": + addValueSet(theResource); + break; + } + } + @Override public List fetchAllConformanceResources() { ArrayList retVal = new ArrayList<>(); @@ -159,7 +179,7 @@ public class PrePopulatedValidationSupport extends BaseStaticResourceValidationS } @Override - public List fetchAllStructureDefinitions() { + public List fetchAllStructureDefinitions() { return toList(myStructureDefinitions); } @@ -187,5 +207,4 @@ public class PrePopulatedValidationSupport extends BaseStaticResourceValidationS public boolean isValueSetSupported(ValidationSupportContext theValidationSupportContext, String theValueSetUrl) { return myValueSets.containsKey(theValueSetUrl); } - } diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/support/PrePopulatedValidationSupportTest.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/support/PrePopulatedValidationSupportTest.java new file mode 100644 index 00000000000..aded98b4f1e --- /dev/null +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/support/PrePopulatedValidationSupportTest.java @@ -0,0 +1,36 @@ +package org.hl7.fhir.common.hapi.validation.support; + +import ca.uhn.fhir.context.FhirContext; +import org.hl7.fhir.r4.model.CodeSystem; +import org.hl7.fhir.r4.model.StructureDefinition; +import org.hl7.fhir.r4.model.ValueSet; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertSame; + +public class PrePopulatedValidationSupportTest { + + private final PrePopulatedValidationSupport mySvc = new PrePopulatedValidationSupport(FhirContext.forR4Cached()); + + @Test + public void testAddResource() { + + CodeSystem cs = new CodeSystem(); + cs.setUrl("http://cs"); + mySvc.addResource(cs); + + ValueSet vs = new ValueSet(); + vs.setUrl("http://vs"); + mySvc.addResource(vs); + + StructureDefinition sd = new StructureDefinition(); + sd.setUrl("http://sd"); + mySvc.addResource(sd); + + assertSame(cs, mySvc.fetchCodeSystem("http://cs")); + assertSame(vs, mySvc.fetchValueSet("http://vs")); + assertSame(sd, mySvc.fetchStructureDefinition("http://sd")); + + } + +} diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/NpmPackageValidationSupportTest.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/NpmPackageValidationSupportTest.java new file mode 100644 index 00000000000..93b9cd4c11a --- /dev/null +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/NpmPackageValidationSupportTest.java @@ -0,0 +1,67 @@ +package org.hl7.fhir.r4.validation; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.context.support.DefaultProfileValidationSupport; +import ca.uhn.fhir.validation.FhirValidator; +import ca.uhn.fhir.validation.ValidationResult; +import org.hl7.fhir.common.hapi.validation.support.CachingValidationSupport; +import org.hl7.fhir.common.hapi.validation.support.CommonCodeSystemsTerminologyService; +import org.hl7.fhir.common.hapi.validation.support.InMemoryTerminologyServerValidationSupport; +import org.hl7.fhir.common.hapi.validation.support.NpmPackageValidationSupport; +import org.hl7.fhir.common.hapi.validation.support.SnapshotGeneratingValidationSupport; +import org.hl7.fhir.common.hapi.validation.support.ValidationSupportChain; +import org.hl7.fhir.common.hapi.validation.validator.FhirInstanceValidator; +import org.hl7.fhir.r4.model.Patient; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; + +public class NpmPackageValidationSupportTest { + + private static final Logger ourLog = LoggerFactory.getLogger(NpmPackageValidationSupportTest.class); + private FhirContext myFhirContext = FhirContext.forR4Cached(); + + @Test + public void testValidateWithPackage() throws IOException { + + // Create an NPM Package Support module and load one package in from + // the classpath + NpmPackageValidationSupport npmPackageSupport = new NpmPackageValidationSupport(myFhirContext); + npmPackageSupport.loadPackageFromClasspath("classpath:package/UK.Core.r4-1.1.0.tgz"); + + // Create a support chain including the NPM Package Support + ValidationSupportChain validationSupportChain = new ValidationSupportChain( + npmPackageSupport, + new DefaultProfileValidationSupport(myFhirContext), + new CommonCodeSystemsTerminologyService(myFhirContext), + new InMemoryTerminologyServerValidationSupport(myFhirContext), + new SnapshotGeneratingValidationSupport(myFhirContext) + ); + CachingValidationSupport validationSupport = new CachingValidationSupport(validationSupportChain); + + // Create a validator + FhirValidator validator = myFhirContext.newValidator(); + FhirInstanceValidator instanceValidator = new FhirInstanceValidator(validationSupport); + validator.registerValidatorModule(instanceValidator); + + // Create a test patient to validate + Patient patient = new Patient(); + patient.getMeta().addProfile("https://fhir.nhs.uk/R4/StructureDefinition/UKCore-Patient"); + // System but not value set for NHS identifier (this should generate an error) + patient.addIdentifier().setSystem("https://fhir.nhs.uk/Id/nhs-number"); + + // Perform the validation + ValidationResult outcome = validator.validateWithResult(patient); + + String outcomeSerialized = myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(outcome.toOperationOutcome()); + ourLog.info(outcomeSerialized); + assertThat(outcomeSerialized, containsString("Patient.identifier:nhsNumber.value: minimum required = 1, but only found 0")); + + } + +} diff --git a/hapi-fhir-validation/src/test/resources/package/UK.Core.r4-1.1.0.tgz b/hapi-fhir-validation/src/test/resources/package/UK.Core.r4-1.1.0.tgz new file mode 100644 index 0000000000000000000000000000000000000000..3f8bba35ea08d4d8dde56e6e6716d155c9b50d06 GIT binary patch literal 28433 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 literal 0 HcmV?d00001