From 3d5a8bb3f86c040f7864dc1365a675c638695eed Mon Sep 17 00:00:00 2001 From: James Agnew Date: Thu, 30 Apr 2020 15:22:41 -0400 Subject: [PATCH] Add UCUM support (#1824) * Add UCUM support * Add changelog * Some cleanup * Test fix * Add flywayDB callback * Add hooks to schema migrator --- .../java/ca/uhn/fhir/util/ClasspathUtil.java | 106 ++++++++++++++++++ .../fhir/validation/SchemaBaseValidator.java | 36 +----- .../ca/uhn/fhir/util/ClasspathUtilTest.java | 61 ++++++++++ .../4_3_0/1824-add-ucum-support.yaml | 5 + .../hapi/fhir/changelog/4_3_0/changes.yaml | 1 + .../validation/validation_support_modules.md | 11 ++ .../ca/uhn/fhir/jpa/migrate/BaseMigrator.java | 15 +++ .../uhn/fhir/jpa/migrate/FlywayMigrator.java | 2 + .../ca/uhn/fhir/jpa/migrate/Migrator.java | 2 + .../uhn/fhir/jpa/migrate/SchemaMigrator.java | 12 +- .../migrate/taskdef/InitializeSchemaTask.java | 3 +- .../main/java/ca/uhn/fhir/test/BaseTest.java | 35 +----- .../CommonCodeSystemsTerminologyService.java | 73 +++++++++++- ...oryTerminologyServerValidationSupport.java | 89 ++++++++++----- .../validation/SchemaBaseValidatorTest.java | 2 +- ...mmonCodeSystemsTerminologyServiceTest.java | 68 +++++++++++ .../FhirInstanceValidatorR4Test.java | 77 ++++++++++++- .../r4/observation-with-body-temp-ucum.json | 38 +++++++ pom.xml | 2 +- 19 files changed, 540 insertions(+), 98 deletions(-) create mode 100644 hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ClasspathUtil.java create mode 100644 hapi-fhir-base/src/test/java/ca/uhn/fhir/util/ClasspathUtilTest.java create mode 100644 hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/4_3_0/1824-add-ucum-support.yaml create mode 100644 hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/support/CommonCodeSystemsTerminologyServiceTest.java create mode 100644 hapi-fhir-validation/src/test/resources/r4/observation-with-body-temp-ucum.json 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 new file mode 100644 index 00000000000..5d3e661dce2 --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ClasspathUtil.java @@ -0,0 +1,106 @@ +package ca.uhn.fhir.util; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.rest.api.EncodingEnum; +import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; +import com.google.common.base.Charsets; +import org.apache.commons.io.IOUtils; +import org.apache.commons.io.input.BOMInputStream; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.annotation.Nonnull; +import java.io.IOException; +import java.io.InputStream; +import java.util.function.Function; +import java.util.zip.GZIPInputStream; + +/** + * Use this API with caution, it may change! + */ +public class ClasspathUtil { + + private static final Logger ourLog = LoggerFactory.getLogger(ClasspathUtil.class); + + public static String loadResource(String theClasspath) { + Function streamTransform = t -> t; + return loadResource(theClasspath, streamTransform); + } + + /** + * Load a classpath resource, throw an {@link InternalErrorException} if not found + */ + @Nonnull + public static InputStream loadResourceAsStream(String theClasspath) { + InputStream retVal = ClasspathUtil.class.getResourceAsStream(theClasspath); + if (retVal == null) { + throw new InternalErrorException("Unable to find classpath resource: " + theClasspath); + } + return retVal; + } + + /** + * Load a classpath resource, throw an {@link InternalErrorException} if not found + */ + @Nonnull + public static String loadResource(String theClasspath, Function theStreamTransform) { + InputStream stream = ClasspathUtil.class.getResourceAsStream(theClasspath); + try { + if (stream == null) { + throw new IOException("Unable to find classpath resource: " + theClasspath); + } + try { + InputStream newStream = theStreamTransform.apply(stream); + return IOUtils.toString(newStream, Charsets.UTF_8); + } finally { + stream.close(); + } + } catch (IOException e) { + throw new InternalErrorException(e); + } + } + + @Nonnull + public static String loadCompressedResource(String theClasspath) { + Function streamTransform = t -> { + try { + return new GZIPInputStream(t); + } catch (IOException e) { + throw new InternalErrorException(e); + } + }; + return loadResource(theClasspath, streamTransform); + } + + @Nonnull + public static T loadResource(FhirContext theCtx, Class theType, String theClasspath) { + String raw = loadResource(theClasspath); + return EncodingEnum.detectEncodingNoDefault(raw).newParser(theCtx).parseResource(theType, raw); + } + + public static void close(InputStream theInput) { + try { + if (theInput != null) { + theInput.close(); + } + } catch (IOException e) { + ourLog.debug("Closing InputStream threw exception", e); + } + } + + public static Function withBom() { + return t -> new BOMInputStream(t); + } + + public static byte[] loadResourceAsByteArray(String theClasspath) { + InputStream stream = loadResourceAsStream(theClasspath); + try { + return IOUtils.toByteArray(stream); + } catch (IOException e) { + throw new InternalErrorException(e); + } finally { + close(stream); + } + } +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/validation/SchemaBaseValidator.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/validation/SchemaBaseValidator.java index e2fe1cb2e97..cec0ff9194d 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/validation/SchemaBaseValidator.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/validation/SchemaBaseValidator.java @@ -23,9 +23,7 @@ package ca.uhn.fhir.validation; import ca.uhn.fhir.context.ConfigurationException; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.rest.api.EncodingEnum; -import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; -import org.apache.commons.io.IOUtils; -import org.apache.commons.io.input.BOMInputStream; +import ca.uhn.fhir.util.ClasspathUtil; import org.hl7.fhir.instance.model.api.IBaseResource; import org.w3c.dom.ls.LSInput; import org.w3c.dom.ls.LSResourceResolver; @@ -41,10 +39,7 @@ import javax.xml.validation.SchemaFactory; import javax.xml.validation.Validator; import java.io.ByteArrayInputStream; import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; import java.io.StringReader; -import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; @@ -152,20 +147,9 @@ public class SchemaBaseValidator implements IValidatorModule { Source loadXml(String theSchemaName) { String pathToBase = myCtx.getVersion().getPathToSchemaDefinitions() + '/' + theSchemaName; ourLog.debug("Going to load resource: {}", pathToBase); - try (InputStream baseIs = FhirValidator.class.getResourceAsStream(pathToBase)) { - if (baseIs == null) { - throw new InternalErrorException("Schema not found. " + RESOURCES_JAR_NOTE); - } - try (BOMInputStream bomInputStream = new BOMInputStream(baseIs, false)) { - try (InputStreamReader baseReader = new InputStreamReader(bomInputStream, StandardCharsets.UTF_8)) { - // Buffer so that we can close the input stream - String contents = IOUtils.toString(baseReader); - return new StreamSource(new StringReader(contents), null); - } - } - } catch (IOException e) { - throw new InternalErrorException(e); - } + + String contents = ClasspathUtil.loadResource(pathToBase, ClasspathUtil.withBom()); + return new StreamSource(new StringReader(contents), null); } @Override @@ -188,16 +172,8 @@ public class SchemaBaseValidator implements IValidatorModule { ourLog.debug("Loading referenced schema file: " + pathToBase); - try (InputStream baseIs = FhirValidator.class.getResourceAsStream(pathToBase)) { - if (baseIs == null) { - throw new InternalErrorException("Schema file not found: " + pathToBase); - } - - byte[] bytes = IOUtils.toByteArray(baseIs); - input.setByteStream(new ByteArrayInputStream(bytes)); - } catch (IOException e) { - throw new InternalErrorException(e); - } + byte[] bytes = ClasspathUtil.loadResourceAsByteArray(pathToBase); + input.setByteStream(new ByteArrayInputStream(bytes)); return input; } diff --git a/hapi-fhir-base/src/test/java/ca/uhn/fhir/util/ClasspathUtilTest.java b/hapi-fhir-base/src/test/java/ca/uhn/fhir/util/ClasspathUtilTest.java new file mode 100644 index 00000000000..c70687c910d --- /dev/null +++ b/hapi-fhir-base/src/test/java/ca/uhn/fhir/util/ClasspathUtilTest.java @@ -0,0 +1,61 @@ +package ca.uhn.fhir.util; + +import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; +import org.junit.Test; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; + +public class ClasspathUtilTest { + + @Test + public void testLoadResourceNotFound() { + try { + ClasspathUtil.loadResource("/FOOOOOO"); + } catch (InternalErrorException e) { + assertEquals("Unable to find classpath resource: /FOOOOOO", e.getMessage()); + } + } + + @Test + public void testLoadResourceAsStreamNotFound() { + try { + ClasspathUtil.loadResourceAsStream("/FOOOOOO"); + } catch (InternalErrorException e) { + assertEquals("Unable to find classpath resource: /FOOOOOO", e.getMessage()); + } + } + + /** + * Should not throw any exception + */ + @Test + public void testClose_Null() { + ClasspathUtil.close(null); + } + + /** + * Should not throw any exception + */ + @Test + public void testClose_Ok() { + ClasspathUtil.close(new ByteArrayInputStream(new byte[]{0,1,2})); + } + + + /** + * Should not throw any exception + */ + @Test + public void testClose_ThrowException() throws IOException { + InputStream is = mock(InputStream.class); + doThrow(new IOException("FOO")).when(is).close(); + ClasspathUtil.close(is); + } + +} diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/4_3_0/1824-add-ucum-support.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/4_3_0/1824-add-ucum-support.yaml new file mode 100644 index 00000000000..70522664e71 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/4_3_0/1824-add-ucum-support.yaml @@ -0,0 +1,5 @@ +--- +type: add +issue: 1824 +title: Native support for UCUM has been added to the validation stack, meaning that UCUM codes can be validated + at runtime without the need for any external validation. diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/4_3_0/changes.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/4_3_0/changes.yaml index bc38dd31845..4d12c1b1907 100644 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/4_3_0/changes.yaml +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/4_3_0/changes.yaml @@ -9,6 +9,7 @@
  • Hibernate Validator (JPA): 5.4.2.Final -> 6.1.3.Final
  • Guava (JPA): 28.0 -> 28.2
  • Spring Boot (Boot): 2.2.0.RELEASE -> 2.2.6.RELEASE
  • +
  • FlywayDB (JPA) 6.1.0 -> 6.4.1
  • " - item: issue: "1583" 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 5b559f560f9..13ad3028476 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 @@ -98,6 +98,17 @@ The following table lists vocabulary that is validated by this module: added in the future, please get in touch if you would like to help. + + Unified Codes for Units of Measure (UCUM) + + ValueSet: (...)/ValueSet/ucum-units +
    + CodeSystem: http://unitsofmeasure.org + + + Codes are validated using the UcumEssenceService provided by the UCUM Java library. + + diff --git a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/BaseMigrator.java b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/BaseMigrator.java index b37a45f2735..d2f4332078f 100644 --- a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/BaseMigrator.java +++ b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/BaseMigrator.java @@ -21,9 +21,13 @@ package ca.uhn.fhir.jpa.migrate; */ import ca.uhn.fhir.jpa.migrate.taskdef.BaseTask; +import org.apache.commons.lang3.Validate; +import org.flywaydb.core.api.callback.Callback; +import javax.annotation.Nonnull; import javax.sql.DataSource; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Objects; @@ -34,6 +38,17 @@ public abstract class BaseMigrator implements IMigrator { private DriverTypeEnum myDriverType; private DataSource myDataSource; private List myExecutedStatements = new ArrayList<>(); + private List myCallbacks = Collections.emptyList(); + + @Nonnull + public List getCallbacks() { + return myCallbacks; + } + + public void setCallbacks(@Nonnull List theCallbacks) { + Validate.notNull(theCallbacks); + myCallbacks = theCallbacks; + } public DataSource getDataSource() { return myDataSource; diff --git a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/FlywayMigrator.java b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/FlywayMigrator.java index 7373e1889ee..4b24a3a3c63 100644 --- a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/FlywayMigrator.java +++ b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/FlywayMigrator.java @@ -25,6 +25,7 @@ import ca.uhn.fhir.jpa.migrate.taskdef.InitializeSchemaTask; import com.google.common.annotations.VisibleForTesting; import org.flywaydb.core.Flyway; import org.flywaydb.core.api.MigrationInfoService; +import org.flywaydb.core.api.callback.Callback; import org.flywaydb.core.api.migration.JavaMigration; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -79,6 +80,7 @@ public class FlywayMigrator extends BaseMigrator { .baselineOnMigrate(true) .outOfOrder(isOutOfOrderPermitted()) .javaMigrations(myTasks.toArray(new JavaMigration[0])) + .callbacks(getCallbacks().toArray(new Callback[0])) .load(); for (FlywayMigration task : myTasks) { task.setConnectionProperties(theConnectionProperties); diff --git a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/Migrator.java b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/Migrator.java index 1688de89a5e..82eab50491d 100644 --- a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/Migrator.java +++ b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/Migrator.java @@ -132,4 +132,6 @@ public class Migrator { public void setNoColumnShrink(boolean theNoColumnShrink) { myNoColumnShrink = theNoColumnShrink; } + + } diff --git a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/SchemaMigrator.java b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/SchemaMigrator.java index 3be8039ac3d..f4b98c3f615 100644 --- a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/SchemaMigrator.java +++ b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/SchemaMigrator.java @@ -24,20 +24,23 @@ import ca.uhn.fhir.context.ConfigurationException; import ca.uhn.fhir.jpa.migrate.taskdef.BaseTask; import org.flywaydb.core.api.MigrationInfo; import org.flywaydb.core.api.MigrationInfoService; +import org.flywaydb.core.api.callback.Callback; import org.hibernate.cfg.AvailableSettings; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.util.Assert; import javax.sql.DataSource; import java.sql.Connection; import java.sql.SQLException; +import java.util.Collections; import java.util.List; import java.util.Optional; import java.util.Properties; public class SchemaMigrator { - private static final Logger ourLog = LoggerFactory.getLogger(SchemaMigrator.class); public static final String HAPI_FHIR_MIGRATION_TABLENAME = "FLY_HFJ_MIGRATION"; + private static final Logger ourLog = LoggerFactory.getLogger(SchemaMigrator.class); private final DataSource myDataSource; private final boolean mySkipValidation; private final String myMigrationTableName; @@ -45,6 +48,7 @@ public class SchemaMigrator { private boolean myDontUseFlyway; private boolean myOutOfOrderPermitted; private DriverTypeEnum myDriverType; + private List myCallbacks = Collections.emptyList(); /** * Constructor @@ -61,6 +65,11 @@ public class SchemaMigrator { } } + public void setCallbacks(List theCallbacks) { + Assert.notNull(theCallbacks); + myCallbacks = theCallbacks; + } + public void setDontUseFlyway(boolean theDontUseFlyway) { myDontUseFlyway = theDontUseFlyway; } @@ -110,6 +119,7 @@ public class SchemaMigrator { migrator.setOutOfOrderPermitted(myOutOfOrderPermitted); } migrator.addTasks(myMigrationTasks); + migrator.setCallbacks(myCallbacks); return migrator; } diff --git a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/InitializeSchemaTask.java b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/InitializeSchemaTask.java index 3a2f28c6474..3664c4c271d 100644 --- a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/InitializeSchemaTask.java +++ b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/InitializeSchemaTask.java @@ -34,13 +34,14 @@ import java.util.Set; public class InitializeSchemaTask extends BaseTask { private static final Logger ourLog = LoggerFactory.getLogger(InitializeSchemaTask.class); + public static final String DESCRIPTION_PREFIX = "Initialize schema for "; private final ISchemaInitializationProvider mySchemaInitializationProvider; public InitializeSchemaTask(String theProductVersion, String theSchemaVersion, ISchemaInitializationProvider theSchemaInitializationProvider) { super(theProductVersion, theSchemaVersion); mySchemaInitializationProvider = theSchemaInitializationProvider; - setDescription("Initialize schema for " + mySchemaInitializationProvider.getSchemaDescription()); + setDescription(DESCRIPTION_PREFIX + mySchemaInitializationProvider.getSchemaDescription()); } @Override diff --git a/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/BaseTest.java b/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/BaseTest.java index 0cbcf44f7b9..6072cd9445d 100644 --- a/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/BaseTest.java +++ b/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/BaseTest.java @@ -21,18 +21,12 @@ package ca.uhn.fhir.test; */ import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.rest.api.EncodingEnum; -import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; -import com.google.common.base.Charsets; -import org.apache.commons.io.IOUtils; +import ca.uhn.fhir.util.ClasspathUtil; import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringStyle; import org.hl7.fhir.instance.model.api.IBaseResource; import java.io.IOException; -import java.io.InputStream; -import java.util.function.Function; -import java.util.zip.GZIPInputStream; public class BaseTest { @@ -41,35 +35,14 @@ public class BaseTest { } protected String loadResource(String theClasspath) throws IOException { - Function streamTransform = t->t; - return loadResource(theClasspath, streamTransform); - } - - private String loadResource(String theClasspath, Function theStreamTransform) throws IOException { - try (InputStream stream = BaseTest.class.getResourceAsStream(theClasspath)) { - if (stream == null) { - throw new IllegalArgumentException("Unable to find resource: " + theClasspath); - } - - InputStream newStream = theStreamTransform.apply(stream); - - return IOUtils.toString(newStream, Charsets.UTF_8); - } + return ClasspathUtil.loadResource(theClasspath); } protected String loadCompressedResource(String theClasspath) throws IOException { - Function streamTransform = t-> { - try { - return new GZIPInputStream(t); - } catch (IOException e) { - throw new InternalErrorException(e); - } - }; - return loadResource(theClasspath, streamTransform); + return ClasspathUtil.loadCompressedResource(theClasspath); } protected T loadResource(FhirContext theCtx, Class theType, String theClasspath) throws IOException { - String raw = loadResource(theClasspath); - return EncodingEnum.detectEncodingNoDefault(raw).newParser(theCtx).parseResource(theType, raw); + return ClasspathUtil.loadResource(theCtx, theType, theClasspath); } } diff --git a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/CommonCodeSystemsTerminologyService.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/CommonCodeSystemsTerminologyService.java index 5de2f64ed70..a007b2990fc 100644 --- a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/CommonCodeSystemsTerminologyService.java +++ b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/CommonCodeSystemsTerminologyService.java @@ -1,13 +1,22 @@ package org.hl7.fhir.common.hapi.validation.support; import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.context.FhirVersionEnum; import ca.uhn.fhir.context.support.ConceptValidationOptions; import ca.uhn.fhir.context.support.IValidationSupport; +import ca.uhn.fhir.util.ClasspathUtil; +import ca.uhn.fhir.util.FileUtil; import org.apache.commons.lang3.Validate; +import org.fhir.ucum.UcumEssenceService; +import org.fhir.ucum.UcumException; import org.hl7.fhir.dstu2.model.ValueSet; import org.hl7.fhir.instance.model.api.IBaseResource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import javax.annotation.Nonnull; +import java.io.IOException; +import java.io.InputStream; import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -26,12 +35,13 @@ public class CommonCodeSystemsTerminologyService implements IValidationSupport { public static final String MIMETYPES_VALUESET_URL = "http://hl7.org/fhir/ValueSet/mimetypes"; public static final String CURRENCIES_CODESYSTEM_URL = "urn:iso:std:iso:4217"; public static final String CURRENCIES_VALUESET_URL = "http://hl7.org/fhir/ValueSet/currencies"; + public static final String UCUM_CODESYSTEM_URL = "http://unitsofmeasure.org"; private static final String USPS_CODESYSTEM_URL = "https://www.usps.com/"; private static final String USPS_VALUESET_URL = "http://hl7.org/fhir/us/core/ValueSet/us-core-usps-state"; + private static final Logger ourLog = LoggerFactory.getLogger(CommonCodeSystemsTerminologyService.class); + public static final String UCUM_VALUESET_URL = "http://hl7.org/fhir/ValueSet/ucum-units"; private static Map USPS_CODES = Collections.unmodifiableMap(buildUspsCodes()); private static Map ISO_4217_CODES = Collections.unmodifiableMap(buildIso4217Codes()); - - private final FhirContext myFhirContext; /** @@ -71,8 +81,22 @@ public class CommonCodeSystemsTerminologyService implements IValidationSupport { return new CodeValidationResult() .setCode(theCode) .setDisplay(theDisplay); - } + case UCUM_VALUESET_URL: { + String system = theCodeSystem; + if (system == null && theOptions.isInferSystem()) { + system = UCUM_CODESYSTEM_URL; + } + LookupCodeResult lookupResult = lookupCode(theRootValidationSupport, system, theCode); + if (lookupResult != null) { + if (lookupResult.isFound()) { + return new CodeValidationResult() + .setCode(lookupResult.getSearchedForCode()) + .setDisplay(lookupResult.getCodeDisplay()); + } + } + } + } if (handlerMap != null) { String display = handlerMap.get(theCode); @@ -92,6 +116,49 @@ public class CommonCodeSystemsTerminologyService implements IValidationSupport { return null; } + @Override + public LookupCodeResult lookupCode(IValidationSupport theRootValidationSupport, String theSystem, String theCode) { + + if (UCUM_CODESYSTEM_URL.equals(theSystem) && theRootValidationSupport.getFhirContext().getVersion().getVersion().isEqualOrNewerThan(FhirVersionEnum.DSTU3)) { + + InputStream input = ClasspathUtil.loadResourceAsStream("/ucum-essence.xml"); + try { + UcumEssenceService svc = new UcumEssenceService(input); + String outcome = svc.analyse(theCode); + if (outcome != null) { + + LookupCodeResult retVal = new LookupCodeResult(); + retVal.setSearchedForCode(theCode); + retVal.setSearchedForSystem(theSystem); + retVal.setFound(true); + retVal.setCodeDisplay(outcome); + return retVal; + + } + } catch (UcumException e) { + ourLog.debug("Failed parse UCUM code: {}", theCode, e); + return null; + } finally { + ClasspathUtil.close(input); + } + + } + + return null; + } + + @Override + public boolean isCodeSystemSupported(IValidationSupport theRootValidationSupport, String theSystem) { + + switch (theSystem) { + case UCUM_CODESYSTEM_URL: + return true; + } + + return false; + } + + public String getValueSetUrl(@Nonnull IBaseResource theValueSet) { String url; switch (getFhirContext().getVersion().getVersion()) { diff --git a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/InMemoryTerminologyServerValidationSupport.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/InMemoryTerminologyServerValidationSupport.java index 0b608ede8b1..909e628e548 100644 --- a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/InMemoryTerminologyServerValidationSupport.java +++ b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/InMemoryTerminologyServerValidationSupport.java @@ -22,6 +22,7 @@ import org.hl7.fhir.utilities.validation.ValidationMessage; import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.util.ArrayList; +import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -53,7 +54,7 @@ public class InMemoryTerminologyServerValidationSupport implements IValidationSu @Override public ValueSetExpansionOutcome expandValueSet(IValidationSupport theRootValidationSupport, ValueSetExpansionOptions theExpansionOptions, IBaseResource theValueSetToExpand) { - org.hl7.fhir.r5.model.ValueSet expansionR5 = expandValueSetToCanonical(theRootValidationSupport, theValueSetToExpand); + org.hl7.fhir.r5.model.ValueSet expansionR5 = expandValueSetToCanonical(theRootValidationSupport, theValueSetToExpand, null, null); if (expansionR5 == null) { return null; } @@ -85,20 +86,20 @@ public class InMemoryTerminologyServerValidationSupport implements IValidationSu return new ValueSetExpansionOutcome(expansion, null); } - private org.hl7.fhir.r5.model.ValueSet expandValueSetToCanonical(IValidationSupport theRootValidationSupport, IBaseResource theValueSetToExpand) { + private org.hl7.fhir.r5.model.ValueSet expandValueSetToCanonical(IValidationSupport theRootValidationSupport, IBaseResource theValueSetToExpand, @Nullable String theWantSystem, @Nullable String theWantCode) { org.hl7.fhir.r5.model.ValueSet expansionR5; switch (myCtx.getVersion().getVersion()) { case DSTU2: case DSTU2_HL7ORG: { - expansionR5 = expandValueSetDstu2Hl7Org(theRootValidationSupport, (ValueSet) theValueSetToExpand); + expansionR5 = expandValueSetDstu2Hl7Org(theRootValidationSupport, (ValueSet) theValueSetToExpand, theWantSystem, theWantCode); break; } case DSTU3: { - expansionR5 = expandValueSetDstu3(theRootValidationSupport, (org.hl7.fhir.dstu3.model.ValueSet) theValueSetToExpand); + expansionR5 = expandValueSetDstu3(theRootValidationSupport, (org.hl7.fhir.dstu3.model.ValueSet) theValueSetToExpand, theWantSystem, theWantCode); break; } case R4: { - expansionR5 = expandValueSetR4(theRootValidationSupport, (org.hl7.fhir.r4.model.ValueSet) theValueSetToExpand); + expansionR5 = expandValueSetR4(theRootValidationSupport, (org.hl7.fhir.r4.model.ValueSet) theValueSetToExpand, theWantSystem, theWantCode); break; } case R5: { @@ -118,7 +119,7 @@ public class InMemoryTerminologyServerValidationSupport implements IValidationSu @Override public CodeValidationResult validateCodeInValueSet(IValidationSupport theRootValidationSupport, ConceptValidationOptions theOptions, String theCodeSystem, String theCode, String theDisplay, @Nonnull IBaseResource theValueSet) { - org.hl7.fhir.r5.model.ValueSet expansion = expandValueSetToCanonical(theRootValidationSupport, theValueSet); + org.hl7.fhir.r5.model.ValueSet expansion = expandValueSetToCanonical(theRootValidationSupport, theValueSet, theCodeSystem, theCode); if (expansion == null) { return null; } @@ -287,7 +288,7 @@ public class InMemoryTerminologyServerValidationSupport implements IValidationSu } @Nullable - private org.hl7.fhir.r5.model.ValueSet expandValueSetDstu2Hl7Org(IValidationSupport theRootValidationSupport, ValueSet theInput) { + private org.hl7.fhir.r5.model.ValueSet expandValueSetDstu2Hl7Org(IValidationSupport theRootValidationSupport, ValueSet theInput, @Nullable String theWantSystem, @Nullable String theWantCode) { Function codeSystemLoader = t -> { org.hl7.fhir.dstu2.model.ValueSet codeSystem = (org.hl7.fhir.dstu2.model.ValueSet) theRootValidationSupport.fetchCodeSystem(t); CodeSystem retVal = new CodeSystem(); @@ -300,7 +301,7 @@ public class InMemoryTerminologyServerValidationSupport implements IValidationSu }; org.hl7.fhir.r5.model.ValueSet input = ValueSet10_50.convertValueSet(theInput); - org.hl7.fhir.r5.model.ValueSet output = expandValueSetR5(input, codeSystemLoader, valueSetLoader); + org.hl7.fhir.r5.model.ValueSet output = expandValueSetR5(theRootValidationSupport, input, codeSystemLoader, valueSetLoader, theWantSystem, theWantCode); return (output); } @@ -342,7 +343,7 @@ public class InMemoryTerminologyServerValidationSupport implements IValidationSu } @Nullable - private org.hl7.fhir.r5.model.ValueSet expandValueSetDstu3(IValidationSupport theRootValidationSupport, org.hl7.fhir.dstu3.model.ValueSet theInput) { + private org.hl7.fhir.r5.model.ValueSet expandValueSetDstu3(IValidationSupport theRootValidationSupport, org.hl7.fhir.dstu3.model.ValueSet theInput, @Nullable String theWantSystem, @Nullable String theWantCode) { Function codeSystemLoader = t -> { org.hl7.fhir.dstu3.model.CodeSystem codeSystem = (org.hl7.fhir.dstu3.model.CodeSystem) theRootValidationSupport.fetchCodeSystem(t); return CodeSystem30_50.convertCodeSystem(codeSystem); @@ -353,12 +354,12 @@ public class InMemoryTerminologyServerValidationSupport implements IValidationSu }; org.hl7.fhir.r5.model.ValueSet input = ValueSet30_50.convertValueSet(theInput); - org.hl7.fhir.r5.model.ValueSet output = expandValueSetR5(input, codeSystemLoader, valueSetLoader); + org.hl7.fhir.r5.model.ValueSet output = expandValueSetR5(theRootValidationSupport, input, codeSystemLoader, valueSetLoader, theWantSystem, theWantCode); return (output); } @Nullable - private org.hl7.fhir.r5.model.ValueSet expandValueSetR4(IValidationSupport theRootValidationSupport, org.hl7.fhir.r4.model.ValueSet theInput) { + private org.hl7.fhir.r5.model.ValueSet expandValueSetR4(IValidationSupport theRootValidationSupport, org.hl7.fhir.r4.model.ValueSet theInput, @Nullable String theWantSystem, @Nullable String theWantCode) { Function codeSystemLoader = t -> { org.hl7.fhir.r4.model.CodeSystem codeSystem = (org.hl7.fhir.r4.model.CodeSystem) theRootValidationSupport.fetchCodeSystem(t); return CodeSystem40_50.convertCodeSystem(codeSystem); @@ -369,7 +370,7 @@ public class InMemoryTerminologyServerValidationSupport implements IValidationSu }; org.hl7.fhir.r5.model.ValueSet input = ValueSet40_50.convertValueSet(theInput); - org.hl7.fhir.r5.model.ValueSet output = expandValueSetR5(input, codeSystemLoader, valueSetLoader); + org.hl7.fhir.r5.model.ValueSet output = expandValueSetR5(theRootValidationSupport, input, codeSystemLoader, valueSetLoader, theWantSystem, theWantCode); return (output); } @@ -378,16 +379,16 @@ public class InMemoryTerminologyServerValidationSupport implements IValidationSu Function codeSystemLoader = t -> (org.hl7.fhir.r5.model.CodeSystem) theRootValidationSupport.fetchCodeSystem(t); Function valueSetLoader = t -> (org.hl7.fhir.r5.model.ValueSet) theRootValidationSupport.fetchValueSet(t); - return expandValueSetR5(theInput, codeSystemLoader, valueSetLoader); + return expandValueSetR5(theRootValidationSupport, theInput, codeSystemLoader, valueSetLoader, null, null); } @Nullable - private org.hl7.fhir.r5.model.ValueSet expandValueSetR5(org.hl7.fhir.r5.model.ValueSet theInput, Function theCodeSystemLoader, Function theValueSetLoader) { + private org.hl7.fhir.r5.model.ValueSet expandValueSetR5(IValidationSupport theRootValidationSupport, org.hl7.fhir.r5.model.ValueSet theInput, Function theCodeSystemLoader, Function theValueSetLoader, @Nullable String theWantSystem, @Nullable String theWantCode) { Set concepts = new HashSet<>(); try { - expandValueSetR5IncludeOrExclude(concepts, theCodeSystemLoader, theValueSetLoader, theInput.getCompose().getInclude(), true); - expandValueSetR5IncludeOrExclude(concepts, theCodeSystemLoader, theValueSetLoader, theInput.getCompose().getExclude(), false); + expandValueSetR5IncludeOrExclude(theRootValidationSupport, concepts, theCodeSystemLoader, theValueSetLoader, theInput.getCompose().getInclude(), true, theWantSystem, theWantCode); + expandValueSetR5IncludeOrExclude(theRootValidationSupport, concepts, theCodeSystemLoader, theValueSetLoader, theInput.getCompose().getExclude(), false, theWantSystem, theWantCode); } catch (ExpansionCouldNotBeCompletedInternallyException e) { return null; } @@ -403,34 +404,70 @@ public class InMemoryTerminologyServerValidationSupport implements IValidationSu return retVal; } - private void expandValueSetR5IncludeOrExclude(Set theConcepts, Function theCodeSystemLoader, Function theValueSetLoader, List theComposeList, boolean theComposeListIsInclude) throws ExpansionCouldNotBeCompletedInternallyException { + private void expandValueSetR5IncludeOrExclude(IValidationSupport theRootValidationSupport, Set theConcepts, Function theCodeSystemLoader, Function theValueSetLoader, List theComposeList, boolean theComposeListIsInclude, @Nullable String theWantSystem, @Nullable String theWantCode) throws ExpansionCouldNotBeCompletedInternallyException { for (org.hl7.fhir.r5.model.ValueSet.ConceptSetComponent nextInclude : theComposeList) { List nextCodeList = new ArrayList<>(); String system = nextInclude.getSystem(); if (isNotBlank(system)) { + + if (theWantSystem != null && !theWantSystem.equals(system)) { + continue; + } + CodeSystem codeSystem = theCodeSystemLoader.apply(system); - if (codeSystem == null) { - throw new ExpansionCouldNotBeCompletedInternallyException(); - } - if (codeSystem.getContent() == CodeSystem.CodeSystemContentMode.NOTPRESENT) { - throw new ExpansionCouldNotBeCompletedInternallyException(); - } Set wantCodes; if (nextInclude.getConcept().isEmpty()) { wantCodes = null; } else { - wantCodes = nextInclude.getConcept().stream().map(t -> t.getCode()).collect(Collectors.toSet()); + wantCodes = nextInclude + .getConcept() + .stream().map(t -> t.getCode()).collect(Collectors.toSet()); + } + + boolean ableToHandleCode = false; + if (codeSystem == null) { + + if (theWantCode != null) { + LookupCodeResult lookup = theRootValidationSupport.lookupCode(theRootValidationSupport, system, theWantCode); + if (lookup != null && lookup.isFound()) { + CodeSystem.ConceptDefinitionComponent conceptDefinition = new CodeSystem.ConceptDefinitionComponent() + .addConcept() + .setCode(theWantCode) + .setDisplay(lookup.getCodeDisplay()); + List codesList = Collections.singletonList(conceptDefinition); + addCodes(system, codesList, nextCodeList, wantCodes); + ableToHandleCode = true; + } + } + + } else { + + ableToHandleCode = true; + + } + + if (!ableToHandleCode) { + throw new ExpansionCouldNotBeCompletedInternallyException(); + } + + if (codeSystem != null) { + + if (codeSystem.getContent() == CodeSystem.CodeSystemContentMode.NOTPRESENT) { + throw new ExpansionCouldNotBeCompletedInternallyException(); + } + + addCodes(system, codeSystem.getConcept(), nextCodeList, wantCodes); + } - addCodes(system, codeSystem.getConcept(), nextCodeList, wantCodes); } for (CanonicalType nextValueSetInclude : nextInclude.getValueSet()) { org.hl7.fhir.r5.model.ValueSet vs = theValueSetLoader.apply(nextValueSetInclude.getValueAsString()); if (vs != null) { - org.hl7.fhir.r5.model.ValueSet subExpansion = expandValueSetR5(vs, theCodeSystemLoader, theValueSetLoader); + org.hl7.fhir.r5.model.ValueSet subExpansion = expandValueSetR5(theRootValidationSupport, vs, theCodeSystemLoader, theValueSetLoader, theWantSystem, theWantCode); if (subExpansion == null) { throw new ExpansionCouldNotBeCompletedInternallyException(); } diff --git a/hapi-fhir-validation/src/test/java/ca/uhn/fhir/validation/SchemaBaseValidatorTest.java b/hapi-fhir-validation/src/test/java/ca/uhn/fhir/validation/SchemaBaseValidatorTest.java index 9b81cbdf8ac..36b50b7cd37 100644 --- a/hapi-fhir-validation/src/test/java/ca/uhn/fhir/validation/SchemaBaseValidatorTest.java +++ b/hapi-fhir-validation/src/test/java/ca/uhn/fhir/validation/SchemaBaseValidatorTest.java @@ -26,7 +26,7 @@ public class SchemaBaseValidatorTest { validator.loadXml("foo.xsd"); fail(); } catch (InternalErrorException e) { - assertThat(e.getMessage(), containsString("Schema not found")); + assertThat(e.getMessage(), containsString("Unable to find classpath resource")); } } } diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/support/CommonCodeSystemsTerminologyServiceTest.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/support/CommonCodeSystemsTerminologyServiceTest.java new file mode 100644 index 00000000000..0834c4de2fc --- /dev/null +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/support/CommonCodeSystemsTerminologyServiceTest.java @@ -0,0 +1,68 @@ +package org.hl7.fhir.common.hapi.validation.support; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.context.support.ConceptValidationOptions; +import ca.uhn.fhir.context.support.IValidationSupport; +import org.hl7.fhir.r4.model.ValueSet; +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +public class CommonCodeSystemsTerminologyServiceTest { + + private CommonCodeSystemsTerminologyService mySvc; + private FhirContext myCtx; + + @Before + public void before() { + myCtx = FhirContext.forR4(); + mySvc = new CommonCodeSystemsTerminologyService(myCtx); + } + + @Test + public void testUcum_LookupCode_Good() { + IValidationSupport.LookupCodeResult outcome = mySvc.lookupCode(myCtx.getValidationSupport(), "http://unitsofmeasure.org", "Cel"); + assertEquals(true, outcome.isFound()); + } + + @Test + public void testUcum_LookupCode_Bad() { + IValidationSupport.LookupCodeResult outcome = mySvc.lookupCode(myCtx.getValidationSupport(), "http://unitsofmeasure.org", "AAAAA"); + assertNull( outcome); + } + + @Test + public void testUcum_LookupCode_UnknownSystem() { + IValidationSupport.LookupCodeResult outcome = mySvc.lookupCode(myCtx.getValidationSupport(), "http://foo", "AAAAA"); + assertNull( outcome); + } + + @Test + public void testUcum_ValidateCode_Good() { + ValueSet vs = new ValueSet(); + vs.setUrl("http://hl7.org/fhir/ValueSet/ucum-units"); + IValidationSupport.CodeValidationResult outcome = mySvc.validateCodeInValueSet(myCtx.getValidationSupport(), new ConceptValidationOptions(), "http://unitsofmeasure.org", "mg", null, vs); + assertEquals(true, outcome.isOk()); + assertEquals("(milligram)", outcome.getDisplay()); + } + + @Test + public void testUcum_ValidateCode_Good_SystemInferred() { + ValueSet vs = new ValueSet(); + vs.setUrl("http://hl7.org/fhir/ValueSet/ucum-units"); + IValidationSupport.CodeValidationResult outcome = mySvc.validateCodeInValueSet(myCtx.getValidationSupport(), new ConceptValidationOptions().setInferSystem(true), null, "mg", null, vs); + assertEquals(true, outcome.isOk()); + assertEquals("(milligram)", outcome.getDisplay()); + } + + @Test + public void testUcum_ValidateCode_Bad() { + ValueSet vs = new ValueSet(); + vs.setUrl("http://hl7.org/fhir/ValueSet/ucum-units"); + IValidationSupport.CodeValidationResult outcome = mySvc.validateCodeInValueSet(myCtx.getValidationSupport(), new ConceptValidationOptions(), "http://unitsofmeasure.org", "aaaaa", null, vs); + assertNull(outcome); + } + +} diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/FhirInstanceValidatorR4Test.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/FhirInstanceValidatorR4Test.java index f9aa04ae046..af4e78c1fa2 100644 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/FhirInstanceValidatorR4Test.java +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/FhirInstanceValidatorR4Test.java @@ -17,19 +17,43 @@ import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.Validate; 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.PrePopulatedValidationSupport; import org.hl7.fhir.common.hapi.validation.support.InMemoryTerminologyServerValidationSupport; +import org.hl7.fhir.common.hapi.validation.support.PrePopulatedValidationSupport; import org.hl7.fhir.common.hapi.validation.support.ValidationSupportChain; +import org.hl7.fhir.common.hapi.validation.validator.FhirInstanceValidator; import org.hl7.fhir.exceptions.FHIRException; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.r4.conformance.ProfileUtilities; import org.hl7.fhir.r4.context.IWorkerContext; import org.hl7.fhir.r4.hapi.ctx.HapiWorkerContext; -import org.hl7.fhir.common.hapi.validation.validator.FhirInstanceValidator; -import org.hl7.fhir.r4.model.*; +import org.hl7.fhir.r4.model.AllergyIntolerance; +import org.hl7.fhir.r4.model.Base; +import org.hl7.fhir.r4.model.Base64BinaryType; +import org.hl7.fhir.r4.model.BooleanType; +import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent; +import org.hl7.fhir.r4.model.CodeSystem; +import org.hl7.fhir.r4.model.CodeType; +import org.hl7.fhir.r4.model.Consent; +import org.hl7.fhir.r4.model.ContactPoint; +import org.hl7.fhir.r4.model.DateTimeType; +import org.hl7.fhir.r4.model.Extension; +import org.hl7.fhir.r4.model.Media; +import org.hl7.fhir.r4.model.Narrative; +import org.hl7.fhir.r4.model.Observation; import org.hl7.fhir.r4.model.Observation.ObservationStatus; +import org.hl7.fhir.r4.model.OperationOutcome; +import org.hl7.fhir.r4.model.Patient; +import org.hl7.fhir.r4.model.Period; +import org.hl7.fhir.r4.model.Practitioner; +import org.hl7.fhir.r4.model.Procedure; +import org.hl7.fhir.r4.model.QuestionnaireResponse; +import org.hl7.fhir.r4.model.Reference; +import org.hl7.fhir.r4.model.RelatedPerson; +import org.hl7.fhir.r4.model.StringType; +import org.hl7.fhir.r4.model.StructureDefinition; import org.hl7.fhir.r4.model.StructureDefinition.StructureDefinitionKind; +import org.hl7.fhir.r4.model.ValueSet; import org.hl7.fhir.r4.model.ValueSet.ValueSetExpansionComponent; import org.hl7.fhir.r4.terminologies.ValueSetExpander; import org.hl7.fhir.r4.utils.FHIRPathEngine; @@ -195,7 +219,16 @@ public class FhirInstanceValidatorR4Test extends BaseTest { when(mockSupport.fetchCodeSystem(nullable(String.class))).thenAnswer(new Answer() { @Override public CodeSystem answer(InvocationOnMock theInvocation) { - CodeSystem retVal = (CodeSystem) myDefaultValidationSupport.fetchCodeSystem((String) theInvocation.getArguments()[0]); + String system = theInvocation.getArgument(0, String.class); + if ("http://loinc.org".equals(system)) { + CodeSystem retVal = new CodeSystem(); + retVal.setUrl("http://loinc.org"); + retVal.setContent(CodeSystem.CodeSystemContentMode.NOTPRESENT); + ourLog.debug("fetchCodeSystem({}) : {}", new Object[]{theInvocation.getArguments()[0], retVal}); + return retVal; + } + + CodeSystem retVal = (CodeSystem) myDefaultValidationSupport.fetchCodeSystem(system); ourLog.debug("fetchCodeSystem({}) : {}", new Object[]{theInvocation.getArguments()[0], retVal}); return retVal; } @@ -216,6 +249,23 @@ public class FhirInstanceValidatorR4Test extends BaseTest { return retVal; } }); + when(mockSupport.lookupCode(any(), any(), any())).thenAnswer(t -> { + String system = t.getArgument(1, String.class); + String code = t.getArgument(2, String.class); + if (myValidConcepts.contains(system + "___" + code)) { + return new IValidationSupport.LookupCodeResult().setFound(true); + } else { + return null; + } + }); + when(mockSupport.validateCodeInValueSet(any(), any(), any(), any(), any(), any())).thenAnswer(t -> { + String system = t.getArgument(2, String.class); + String code = t.getArgument(3, String.class); + if (myValidConcepts.contains(system + "___" + code)) { + return new IValidationSupport.CodeValidationResult().setCode(code).setDisplay(code); + } + return null; + }); } @@ -1239,6 +1289,25 @@ public class FhirInstanceValidatorR4Test extends BaseTest { } + + @Test + public void testValidateWithUcum() throws IOException { + addValidConcept("http://loinc.org", "8310-5"); + + Observation input = loadResource(ourCtx, Observation.class, "/r4/observation-with-body-temp-ucum.json"); + ValidationResult output = myVal.validateWithResult(input); + List all = logResultsAndReturnNonInformationalOnes(output); + assertThat(all, empty()); + + // Change the unit to something not supported + input.getValueQuantity().setCode("Heck"); + output = myVal.validateWithResult(input); + all = logResultsAndReturnNonInformationalOnes(output); + assertEquals(1, all.size()); + assertThat(all.get(0).getMessage(), containsString("The value provided (\"Heck\") is not in the value set http://hl7.org/fhir/ValueSet/ucum-bodytemp")); + + } + @Test public void testMultiplePerformer() { Observation o = new Observation(); diff --git a/hapi-fhir-validation/src/test/resources/r4/observation-with-body-temp-ucum.json b/hapi-fhir-validation/src/test/resources/r4/observation-with-body-temp-ucum.json new file mode 100644 index 00000000000..2f6e9883305 --- /dev/null +++ b/hapi-fhir-validation/src/test/resources/r4/observation-with-body-temp-ucum.json @@ -0,0 +1,38 @@ +{ + "resourceType": "Observation", + "id": "bodytemp", + "meta": { + "profile": [ + "http://hl7.org/fhir/StructureDefinition/bodytemp" + ] + }, + "status": "final", + "category": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/observation-category", + "code": "vital-signs" + } + ] + } + ], + "code": { + "coding": [ + { + "system": "http://loinc.org", + "code": "8310-5" + } + ] + }, + "subject": { + "reference": "Patient/1" + }, + "effectiveDateTime": "2020-04-30T12:00:00+01:00", + "valueQuantity": { + "value": 37.5, + "unit": "Cel", + "system": "http://unitsofmeasure.org", + "code": "Cel" + } +} diff --git a/pom.xml b/pom.xml index 5bd3e7996b9..f6b45adfbb0 100644 --- a/pom.xml +++ b/pom.xml @@ -664,7 +664,7 @@ 9.4.24.v20191120 3.0.2 - 6.1.0 + 6.4.1 5.4.14.Final