Add UCUM support (#1824)

* Add UCUM support

* Add changelog

* Some cleanup

* Test fix

* Add flywayDB callback

* Add hooks to schema migrator
This commit is contained in:
James Agnew 2020-04-30 15:22:41 -04:00 committed by GitHub
parent f94f2fde65
commit 3d5a8bb3f8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 540 additions and 98 deletions

View File

@ -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<InputStream, InputStream> 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<InputStream, InputStream> 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<InputStream, InputStream> streamTransform = t -> {
try {
return new GZIPInputStream(t);
} catch (IOException e) {
throw new InternalErrorException(e);
}
};
return loadResource(theClasspath, streamTransform);
}
@Nonnull
public static <T extends IBaseResource> T loadResource(FhirContext theCtx, Class<T> 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<InputStream, InputStream> 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);
}
}
}

View File

@ -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,21 +147,10 @@ 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);
String contents = ClasspathUtil.loadResource(pathToBase, ClasspathUtil.withBom());
return new StreamSource(new StringReader(contents), null);
}
}
} catch (IOException e) {
throw new InternalErrorException(e);
}
}
@Override
public void validateResource(IValidationContext<IBaseResource> theContext) {
@ -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);
byte[] bytes = ClasspathUtil.loadResourceAsByteArray(pathToBase);
input.setByteStream(new ByteArrayInputStream(bytes));
} catch (IOException e) {
throw new InternalErrorException(e);
}
return input;
}

View File

@ -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);
}
}

View File

@ -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.

View File

@ -9,6 +9,7 @@
<li>Hibernate Validator (JPA): 5.4.2.Final -&gt; 6.1.3.Final</li>
<li>Guava (JPA): 28.0 -&gt; 28.2</li>
<li>Spring Boot (Boot): 2.2.0.RELEASE -&gt; 2.2.6.RELEASE</li>
<li>FlywayDB (JPA) 6.1.0 -&gt; 6.4.1</li>
</ul>"
- item:
issue: "1583"

View File

@ -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.
</td>
</tr>
<tr>
<td>Unified Codes for Units of Measure (UCUM)</td>
<td>
ValueSet: <code><a href="http://hl7.org/fhir/ValueSet/ucum-units">(...)/ValueSet/ucum-units</a></code>
<br/>
CodeSystem: <code>http://unitsofmeasure.org</code>
</td>
<td>
Codes are validated using the UcumEssenceService provided by the <a href="https://github.com/FHIR/Ucum-java">UCUM Java</a> library.
</td>
</tr>
</tbody>
</table>

View File

@ -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<BaseTask.ExecutedStatement> myExecutedStatements = new ArrayList<>();
private List<Callback> myCallbacks = Collections.emptyList();
@Nonnull
public List<Callback> getCallbacks() {
return myCallbacks;
}
public void setCallbacks(@Nonnull List<Callback> theCallbacks) {
Validate.notNull(theCallbacks);
myCallbacks = theCallbacks;
}
public DataSource getDataSource() {
return myDataSource;

View File

@ -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);

View File

@ -132,4 +132,6 @@ public class Migrator {
public void setNoColumnShrink(boolean theNoColumnShrink) {
myNoColumnShrink = theNoColumnShrink;
}
}

View File

@ -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<Callback> myCallbacks = Collections.emptyList();
/**
* Constructor
@ -61,6 +65,11 @@ public class SchemaMigrator {
}
}
public void setCallbacks(List<Callback> 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;
}

View File

@ -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

View File

@ -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<InputStream, InputStream> streamTransform = t->t;
return loadResource(theClasspath, streamTransform);
}
private String loadResource(String theClasspath, Function<InputStream, InputStream> 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<InputStream, InputStream> streamTransform = t-> {
try {
return new GZIPInputStream(t);
} catch (IOException e) {
throw new InternalErrorException(e);
}
};
return loadResource(theClasspath, streamTransform);
return ClasspathUtil.loadCompressedResource(theClasspath);
}
protected <T extends IBaseResource> T loadResource(FhirContext theCtx, Class<T> theType, String theClasspath) throws IOException {
String raw = loadResource(theClasspath);
return EncodingEnum.detectEncodingNoDefault(raw).newParser(theCtx).parseResource(theType, raw);
return ClasspathUtil.loadResource(theCtx, theType, theClasspath);
}
}

View File

@ -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<String, String> USPS_CODES = Collections.unmodifiableMap(buildUspsCodes());
private static Map<String, String> 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()) {

View File

@ -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<String, CodeSystem> 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<String, org.hl7.fhir.r5.model.CodeSystem> 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<String, org.hl7.fhir.r5.model.CodeSystem> 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<String, org.hl7.fhir.r5.model.CodeSystem> codeSystemLoader = t -> (org.hl7.fhir.r5.model.CodeSystem) theRootValidationSupport.fetchCodeSystem(t);
Function<String, org.hl7.fhir.r5.model.ValueSet> 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<String, CodeSystem> theCodeSystemLoader, Function<String, org.hl7.fhir.r5.model.ValueSet> theValueSetLoader) {
private org.hl7.fhir.r5.model.ValueSet expandValueSetR5(IValidationSupport theRootValidationSupport, org.hl7.fhir.r5.model.ValueSet theInput, Function<String, CodeSystem> theCodeSystemLoader, Function<String, org.hl7.fhir.r5.model.ValueSet> theValueSetLoader, @Nullable String theWantSystem, @Nullable String theWantCode) {
Set<VersionIndependentConcept> 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<VersionIndependentConcept> theConcepts, Function<String, CodeSystem> theCodeSystemLoader, Function<String, org.hl7.fhir.r5.model.ValueSet> theValueSetLoader, List<org.hl7.fhir.r5.model.ValueSet.ConceptSetComponent> theComposeList, boolean theComposeListIsInclude) throws ExpansionCouldNotBeCompletedInternallyException {
private void expandValueSetR5IncludeOrExclude(IValidationSupport theRootValidationSupport, Set<VersionIndependentConcept> theConcepts, Function<String, CodeSystem> theCodeSystemLoader, Function<String, org.hl7.fhir.r5.model.ValueSet> theValueSetLoader, List<org.hl7.fhir.r5.model.ValueSet.ConceptSetComponent> theComposeList, boolean theComposeListIsInclude, @Nullable String theWantSystem, @Nullable String theWantCode) throws ExpansionCouldNotBeCompletedInternallyException {
for (org.hl7.fhir.r5.model.ValueSet.ConceptSetComponent nextInclude : theComposeList) {
List<VersionIndependentConcept> 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<String> 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<CodeSystem.ConceptDefinitionComponent> 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);
}
}
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();
}

View File

@ -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"));
}
}
}

View File

@ -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);
}
}

View File

@ -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<CodeSystem>() {
@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<SingleValidationMessage> 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();

View File

@ -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"
}
}

View File

@ -664,7 +664,7 @@
<!-- 9.4.17 seems to have issues -->
<jetty_version>9.4.24.v20191120</jetty_version>
<jsr305_version>3.0.2</jsr305_version>
<flyway_version>6.1.0</flyway_version>
<flyway_version>6.4.1</flyway_version>
<!--<hibernate_version>5.2.10.Final</hibernate_version>-->
<hibernate_version>5.4.14.Final</hibernate_version>
<!-- Update lucene version when you update hibernate-search version -->