Correctly validate ISO 3166 codes (#1967 update) (#1973)

* Add tests

* Test fixes

* Fix tests

* Test fix

* One more test fix
This commit is contained in:
James Agnew 2020-07-09 18:34:26 -04:00 committed by GitHub
parent c40d15294d
commit c949349a41
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 261 additions and 69 deletions

View File

@ -29,6 +29,7 @@ import org.springframework.batch.core.JobParametersInvalidException;
import org.springframework.batch.core.JobParametersValidator;
import org.springframework.beans.factory.annotation.Autowired;
import javax.transaction.Transactional;
import java.util.Arrays;
import java.util.Optional;
@ -40,6 +41,7 @@ public class BulkExportJobParameterValidator implements JobParametersValidator {
private IBulkExportJobDao myBulkExportJobDao;
@Override
@Transactional
public void validate(JobParameters theJobParameters) throws JobParametersInvalidException {
if (theJobParameters == null) {
throw new JobParametersInvalidException("This job needs Parameters: [readChunkSize], [jobUUID], [filters], [outputFormat], [resourceTypes]");

View File

@ -660,9 +660,14 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc {
if (includedConcepts != null) {
int foundCount = 0;
for (VersionIndependentConcept next : includedConcepts) {
LookupCodeResult lookup = myValidationSupport.lookupCode(new ValidationSupportContext(myValidationSupport), next.getSystem(), next.getCode());
String nextSystem = next.getSystem();
if (nextSystem == null) {
nextSystem = system;
}
LookupCodeResult lookup = myValidationSupport.lookupCode(new ValidationSupportContext(myValidationSupport), nextSystem, next.getCode());
if (lookup != null && lookup.isFound()) {
addOrRemoveCode(theValueSetCodeAccumulator, theAddedCodes, theAdd, next.getSystem(), next.getCode(), lookup.getCodeDisplay());
addOrRemoveCode(theValueSetCodeAccumulator, theAddedCodes, theAdd, nextSystem, next.getCode(), lookup.getCodeDisplay());
foundCount++;
}
}

View File

@ -66,13 +66,13 @@ public class JpaValidationSupportChain extends ValidationSupportChain {
@PostConstruct
public void postConstruct() {
addValidationSupport(new CommonCodeSystemsTerminologyService(myFhirContext));
addValidationSupport(myDefaultProfileValidationSupport);
addValidationSupport(myJpaValidationSupport);
addValidationSupport(myTerminologyService);
addValidationSupport(new SnapshotGeneratingValidationSupport(myFhirContext));
addValidationSupport(new InMemoryTerminologyServerValidationSupport(myFhirContext));
addValidationSupport(myNpmJpaValidationSupport);
addValidationSupport(new CommonCodeSystemsTerminologyService(myFhirContext));
}
}

View File

@ -28,6 +28,7 @@ import ca.uhn.fhir.jpa.dao.data.ITermCodeSystemDao;
import ca.uhn.fhir.jpa.dao.data.ITermConceptDao;
import ca.uhn.fhir.jpa.dao.data.ITermConceptMapDao;
import ca.uhn.fhir.jpa.dao.data.ITermConceptMapGroupElementTargetDao;
import ca.uhn.fhir.jpa.dao.data.ITermValueSetDao;
import ca.uhn.fhir.jpa.dao.dstu2.FhirResourceDaoDstu2SearchNoFtTest;
import ca.uhn.fhir.jpa.dao.r4.BaseJpaR4Test;
import ca.uhn.fhir.jpa.model.entity.ModelConfig;
@ -340,6 +341,8 @@ public abstract class BaseJpaDstu3Test extends BaseJpaTest {
private IValidationSupport myJpaValidationSupportChainDstu3;
@Autowired
private IBulkDataExportSvc myBulkDataExportSvc;
@Autowired
protected ITermValueSetDao myTermValueSetDao;
@AfterEach()
public void afterCleanupDao() {

View File

@ -2,8 +2,9 @@ package ca.uhn.fhir.jpa.dao.dstu3;
import ca.uhn.fhir.context.support.IValidationSupport;
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDaoValueSet;
import ca.uhn.fhir.jpa.entity.TermValueSet;
import ca.uhn.fhir.jpa.entity.TermValueSetPreExpansionStatusEnum;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import ca.uhn.fhir.util.TestUtil;
import org.hl7.fhir.dstu3.model.CodeSystem;
import org.hl7.fhir.dstu3.model.CodeType;
import org.hl7.fhir.dstu3.model.CodeableConcept;
@ -14,7 +15,6 @@ import org.hl7.fhir.dstu3.model.UriType;
import org.hl7.fhir.dstu3.model.ValueSet;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
@ -52,6 +52,51 @@ public class FhirResourceDaoDstu3ValueSetTest extends BaseJpaDstu3Test {
}
@Test
public void testExpandValueSetWithIso3166() throws IOException {
ValueSet vs = loadResourceFromClasspath(ValueSet.class, "/dstu3/nl/LandISOCodelijst-2.16.840.1.113883.2.4.3.11.60.40.2.20.5.2--20171231000000.json");
myValueSetDao.create(vs);
runInTransaction(() -> {
TermValueSet vsEntity = myTermValueSetDao.findByUrl("http://decor.nictiz.nl/fhir/ValueSet/2.16.840.1.113883.2.4.3.11.60.40.2.20.5.2--20171231000000").orElseThrow(() -> new IllegalStateException());
assertEquals(TermValueSetPreExpansionStatusEnum.NOT_EXPANDED, vsEntity.getExpansionStatus());
});
IFhirResourceDaoValueSet.ValidateCodeResult validationOutcome;
UriType vsIdentifier = new UriType("http://decor.nictiz.nl/fhir/ValueSet/2.16.840.1.113883.2.4.3.11.60.40.2.20.5.2--20171231000000");
CodeType code = new CodeType();
CodeType system = new CodeType("urn:iso:std:iso:3166");
// Validate good
code.setValue("NL");
validationOutcome = myValueSetDao.validateCode(vsIdentifier, null, code, system, null, null, null, mySrd);
assertEquals(true, validationOutcome.isResult());
// Validate bad
code.setValue("QQ");
validationOutcome = myValueSetDao.validateCode(vsIdentifier, null, code, system, null, null, null, mySrd);
assertEquals(false, validationOutcome.isResult());
myTermSvc.preExpandDeferredValueSetsToTerminologyTables();
runInTransaction(() -> {
TermValueSet vsEntity = myTermValueSetDao.findByUrl("http://decor.nictiz.nl/fhir/ValueSet/2.16.840.1.113883.2.4.3.11.60.40.2.20.5.2--20171231000000").orElseThrow(() -> new IllegalStateException());
assertEquals(TermValueSetPreExpansionStatusEnum.EXPANDED, vsEntity.getExpansionStatus());
});
// Validate good
code.setValue("NL");
validationOutcome = myValueSetDao.validateCode(vsIdentifier, null, code, system, null, null, null, mySrd);
assertEquals(true, validationOutcome.isResult());
// Validate bad
code.setValue("QQ");
validationOutcome = myValueSetDao.validateCode(vsIdentifier, null, code, system, null, null, null, mySrd);
assertEquals(false, validationOutcome.isResult());
}
@Test
@Disabled
public void testBuiltInValueSetFetchAndExpand() {
@ -257,6 +302,5 @@ public class FhirResourceDaoDstu3ValueSetTest extends BaseJpaDstu3Test {
}
}

View File

@ -0,0 +1,55 @@
{
"resourceType": "ValueSet",
"id": "2.16.840.1.113883.2.4.3.11.60.40.2.20.5.2--20171231000000",
"meta": {
"profile": [
"http://hl7.org/fhir/StructureDefinition/shareablevalueset"
]
},
"extension": [
{
"url": "http://hl7.org/fhir/StructureDefinition/resource-effectivePeriod",
"valuePeriod": {
"start": "2017-12-31T00:00:00+02:00"
}
}
],
"url": "http://decor.nictiz.nl/fhir/ValueSet/2.16.840.1.113883.2.4.3.11.60.40.2.20.5.2--20171231000000",
"identifier": [
{
"use": "official",
"system": "http://art-decor.org/ns/oids/vs",
"value": "2.16.840.1.113883.2.4.3.11.60.40.2.20.5.2"
}
],
"version": "2017-12-31T00:00:00",
"name": "LandISOCodelijst",
"title": "LandISOCodelijst",
"status": "active",
"experimental": false,
"publisher": "Registratie aan de bron",
"contact": [
{
"name": "Registratie aan de bron",
"telecom": [
{
"system": "url",
"value": "https://www.registratieaandebron.nl"
},
{
"system": "url",
"value": "https://zibs.nl"
}
]
}
],
"description": "ISO 3166-1 (alpha-2) - Alle waarden",
"immutable": false,
"compose": {
"include": [
{
"system": "urn:iso:std:iso:3166"
}
]
}
}

View File

@ -8,8 +8,11 @@ import ca.uhn.fhir.util.ClasspathUtil;
import org.apache.commons.lang3.Validate;
import org.fhir.ucum.UcumEssenceService;
import org.fhir.ucum.UcumException;
import org.hl7.fhir.convertors.VersionConvertor_30_40;
import org.hl7.fhir.convertors.VersionConvertor_40_50;
import org.hl7.fhir.dstu2.model.ValueSet;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.model.CodeSystem;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -121,9 +124,7 @@ public class CommonCodeSystemsTerminologyService implements IValidationSupport {
if (isBlank(theValueSetUrl)) {
CodeValidationResult validationResult = validateLookupCode(theValidationSupportContext, theCode, theCodeSystem);
if (validationResult != null) {
return validationResult;
}
return validationResult;
}
return null;
@ -147,70 +148,33 @@ public class CommonCodeSystemsTerminologyService implements IValidationSupport {
@Override
public LookupCodeResult lookupCode(ValidationSupportContext theValidationSupportContext, String theSystem, String theCode) {
Map<String, String> map;
switch (theSystem) {
case UCUM_CODESYSTEM_URL:
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);
}
break;
case COUNTRIES_CODESYSTEM_URL:
String display = ISO_3166_CODES.get(theCode);
if (isNotBlank(display)) {
LookupCodeResult retVal = new LookupCodeResult();
retVal.setSearchedForCode(theCode);
retVal.setSearchedForSystem(theSystem);
retVal.setFound(true);
retVal.setCodeDisplay(display);
return retVal;
}
break;
return lookupUcumCode(theCode);
case MIMETYPES_CODESYSTEM_URL:
// This is a pretty naive implementation - Should be enhanced in future
LookupCodeResult mimeRetVal = new LookupCodeResult();
mimeRetVal.setSearchedForCode(theCode);
mimeRetVal.setSearchedForSystem(theSystem);
mimeRetVal.setFound(true);
return mimeRetVal;
case CURRENCIES_CODESYSTEM_URL:
String currenciesDisplay = ISO_3166_CODES.get(theCode);
if (isNotBlank(currenciesDisplay)) {
LookupCodeResult retVal = new LookupCodeResult();
retVal.setSearchedForCode(theCode);
retVal.setSearchedForSystem(theSystem);
retVal.setFound(true);
retVal.setCodeDisplay(currenciesDisplay);
return retVal;
}
return lookupMimetypeCode(theCode);
case COUNTRIES_CODESYSTEM_URL:
map = ISO_3166_CODES;
break;
case CURRENCIES_CODESYSTEM_URL:
map = ISO_4217_CODES;
break;
case USPS_CODESYSTEM_URL:
map = USPS_CODES;
break;
default:
return null;
}
String display = map.get(theCode);
if (isNotBlank(display)) {
LookupCodeResult retVal = new LookupCodeResult();
retVal.setSearchedForCode(theCode);
retVal.setSearchedForSystem(theSystem);
retVal.setFound(true);
retVal.setCodeDisplay(display);
return retVal;
}
// If we get here it means we know the codesystem but the code was bad
@ -222,6 +186,82 @@ public class CommonCodeSystemsTerminologyService implements IValidationSupport {
}
@Nonnull
private LookupCodeResult lookupMimetypeCode(String theCode) {
// This is a pretty naive implementation - Should be enhanced in future
LookupCodeResult mimeRetVal = new LookupCodeResult();
mimeRetVal.setSearchedForCode(theCode);
mimeRetVal.setSearchedForSystem(MIMETYPES_CODESYSTEM_URL);
mimeRetVal.setFound(true);
return mimeRetVal;
}
@Nonnull
private LookupCodeResult lookupUcumCode(String theCode) {
InputStream input = ClasspathUtil.loadResourceAsStream("/ucum-essence.xml");
String outcome = null;
try {
UcumEssenceService svc = new UcumEssenceService(input);
outcome = svc.analyse(theCode);
} catch (UcumException e) {
ourLog.warn("Failed parse UCUM code: {}", theCode, e);
} finally {
ClasspathUtil.close(input);
}
LookupCodeResult retVal = new LookupCodeResult();
retVal.setSearchedForCode(theCode);
retVal.setSearchedForSystem(UCUM_CODESYSTEM_URL);
if (outcome != null) {
retVal.setFound(true);
retVal.setCodeDisplay(outcome);
}
return retVal;
}
@Override
public IBaseResource fetchCodeSystem(String theSystem) {
Map<String, String> map;
switch (defaultString(theSystem)) {
case COUNTRIES_CODESYSTEM_URL:
map = ISO_3166_CODES;
break;
case CURRENCIES_CODESYSTEM_URL:
map = ISO_4217_CODES;
break;
default:
return null;
}
CodeSystem retVal = new CodeSystem();
retVal.setContent(CodeSystem.CodeSystemContentMode.COMPLETE);
retVal.setUrl(theSystem);
for (Map.Entry<String, String> nextEntry : map.entrySet()) {
retVal.addConcept().setCode(nextEntry.getKey()).setDisplay(nextEntry.getValue());
}
IBaseResource normalized = null;
switch (getFhirContext().getVersion().getVersion()) {
case DSTU2:
case DSTU2_HL7ORG:
case DSTU2_1:
return null;
case DSTU3:
normalized = VersionConvertor_30_40.convertResource(retVal, false);
break;
case R4:
normalized = retVal;
break;
case R5:
normalized = VersionConvertor_40_50.convertResource(retVal);
break;
}
Validate.notNull(normalized);
return normalized;
}
@Override
public boolean isCodeSystemSupported(ValidationSupportContext theValidationSupportContext, String theSystem) {
@ -229,6 +269,7 @@ public class CommonCodeSystemsTerminologyService implements IValidationSupport {
case COUNTRIES_CODESYSTEM_URL:
case UCUM_CODESYSTEM_URL:
case MIMETYPES_CODESYSTEM_URL:
case USPS_CODESYSTEM_URL:
return true;
}

View File

@ -1,9 +1,12 @@
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.context.support.ValidationSupportContext;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.model.CodeSystem;
import org.hl7.fhir.r4.model.ValueSet;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@ -37,13 +40,13 @@ public class CommonCodeSystemsTerminologyServiceTest {
@Test
public void testUcum_LookupCode_Bad() {
IValidationSupport.LookupCodeResult outcome = mySvc.lookupCode(new ValidationSupportContext(myCtx.getValidationSupport()), "http://unitsofmeasure.org", "AAAAA");
assertNull( outcome);
assertEquals(false, outcome.isFound());
}
@Test
public void testUcum_LookupCode_UnknownSystem() {
IValidationSupport.LookupCodeResult outcome = mySvc.lookupCode(new ValidationSupportContext(myCtx.getValidationSupport()), "http://foo", "AAAAA");
assertNull( outcome);
assertNull(outcome);
}
@Test
@ -72,4 +75,43 @@ public class CommonCodeSystemsTerminologyServiceTest {
assertNull(outcome);
}
@Test
public void testFetchCodeSystemBuiltIn_Iso3166_R4() {
CodeSystem cs = (CodeSystem) mySvc.fetchCodeSystem(CommonCodeSystemsTerminologyService.COUNTRIES_CODESYSTEM_URL);
assertEquals(498, cs.getConcept().size());
}
@Test
public void testFetchCodeSystemBuiltIn_Iso3166_DSTU3() {
CommonCodeSystemsTerminologyService svc = new CommonCodeSystemsTerminologyService(FhirContext.forCached(FhirVersionEnum.DSTU3));
org.hl7.fhir.dstu3.model.CodeSystem cs = (org.hl7.fhir.dstu3.model.CodeSystem) svc.fetchCodeSystem(CommonCodeSystemsTerminologyService.COUNTRIES_CODESYSTEM_URL);
assertEquals(498, cs.getConcept().size());
}
@Test
public void testFetchCodeSystemBuiltIn_Iso3166_R5() {
CommonCodeSystemsTerminologyService svc = new CommonCodeSystemsTerminologyService(FhirContext.forCached(FhirVersionEnum.R5));
org.hl7.fhir.r5.model.CodeSystem cs = (org.hl7.fhir.r5.model.CodeSystem) svc.fetchCodeSystem(CommonCodeSystemsTerminologyService.COUNTRIES_CODESYSTEM_URL);
assertEquals(498, cs.getConcept().size());
}
@Test
public void testFetchCodeSystemBuiltIn_Iso3166_DSTU2() {
CommonCodeSystemsTerminologyService svc = new CommonCodeSystemsTerminologyService(FhirContext.forCached(FhirVersionEnum.DSTU2));
IBaseResource cs = svc.fetchCodeSystem(CommonCodeSystemsTerminologyService.COUNTRIES_CODESYSTEM_URL);
assertEquals(null, cs);
}
@Test
public void testFetchCodeSystemBuiltIn_Iso_R4() {
CodeSystem cs = (CodeSystem) mySvc.fetchCodeSystem(CommonCodeSystemsTerminologyService.CURRENCIES_CODESYSTEM_URL);
assertEquals(182, cs.getConcept().size());
}
@Test
public void testFetchCodeSystemBuiltIn_Unknown() {
CodeSystem cs = (CodeSystem) mySvc.fetchCodeSystem("http://foo");
assertEquals(null, cs);
}
}

View File

@ -352,7 +352,7 @@ public class FhirInstanceValidatorDstu3Test {
List<SingleValidationMessage> all = logResultsAndReturnAll(result);
assertEquals(1, all.size());
assertEquals(ResultSeverityEnum.ERROR, all.get(0).getSeverity());
assertEquals("Validation failed for \"urn:iso:std:iso:3166#QQ\"", all.get(0).getMessage());
assertEquals("Unknown code 'urn:iso:std:iso:3166#QQ' for \"urn:iso:std:iso:3166#QQ\"", all.get(0).getMessage());
}
}