Resolve validation errors to ValueSet with UCUM (#1948)

* Add tests for validation errors

* Work on validation errors

* Bump core version

* Fix validation errors

* Test fixes

* Add changelog

* Test fix

* Test fix

* Test fix
This commit is contained in:
James Agnew 2020-07-05 19:59:21 -04:00 committed by GitHub
parent 9e19b87e67
commit 6fb6675b1e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 3878 additions and 205 deletions

View File

@ -0,0 +1,5 @@
---
type: fix
issue: 1948
title: "When validating resources containing codes in a ValueSet that included UCUM codes, the validator would
incorrectly report that the code was valid even if it was not in the ValueSet. This has been corrected."

View File

@ -89,7 +89,6 @@ import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Stopwatch;
import net.bytebuddy.implementation.bytecode.Throw;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.apache.commons.lang3.time.DateUtils;
@ -625,128 +624,55 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc {
TermCodeSystem cs = myCodeSystemDao.findByCodeSystemUri(system);
if (cs != null) {
TermCodeSystemVersion csv = cs.getCurrentVersion();
FullTextEntityManager em = org.hibernate.search.jpa.Search.getFullTextEntityManager(myEntityManager);
/*
* If FullText searching is not enabled, we can handle only basic expansions
* since we're going to do it without the database.
*/
if (myFulltextSearchSvc == null) {
expandWithoutHibernateSearch(theValueSetCodeAccumulator, csv, theAddedCodes, theIncludeOrExclude, system, theAdd, theCodeCounter);
return false;
}
/*
* Ok, let's use hibernate search to build the expansion
*/
QueryBuilder qb = em.getSearchFactory().buildQueryBuilder().forEntity(TermConcept.class).get();
BooleanJunction<?> bool = qb.bool();
bool.must(qb.keyword().onField("myCodeSystemVersionPid").matching(csv.getPid()).createQuery());
if (theWantConceptOrNull != null) {
bool.must(qb.keyword().onField("myCode").matching(theWantConceptOrNull.getCode()).createQuery());
}
/*
* Filters
*/
handleFilters(bool, system, qb, theIncludeOrExclude);
Query luceneQuery = bool.createQuery();
/*
* Include/Exclude Concepts
*/
List<Term> codes = theIncludeOrExclude
.getConcept()
.stream()
.filter(Objects::nonNull)
.map(ValueSet.ConceptReferenceComponent::getCode)
.filter(StringUtils::isNotBlank)
.map(t -> new Term("myCode", t))
.collect(Collectors.toList());
if (codes.size() > 0) {
BooleanQuery.Builder builder = new BooleanQuery.Builder();
builder.setMinimumNumberShouldMatch(1);
for (Term nextCode : codes) {
builder.add(new TermQuery(nextCode), BooleanClause.Occur.SHOULD);
}
luceneQuery = new BooleanQuery.Builder()
.add(luceneQuery, BooleanClause.Occur.MUST)
.add(builder.build(), BooleanClause.Occur.MUST)
.build();
}
/*
* Execute the query
*/
FullTextQuery jpaQuery = em.createFullTextQuery(luceneQuery, TermConcept.class);
/*
* DM 2019-08-21 - Processing slows after any ValueSets with many codes explicitly identified. This might
* be due to the dark arts that is memory management. Will monitor but not do anything about this right now.
*/
BooleanQuery.setMaxClauseCount(10000);
StopWatch sw = new StopWatch();
AtomicInteger count = new AtomicInteger(0);
int maxResultsPerBatch = 10000;
/*
* If the accumulator is bounded, we may reduce the size of the query to
* Lucene in order to be more efficient.
*/
if (theAdd) {
Integer accumulatorCapacityRemaining = theValueSetCodeAccumulator.getCapacityRemaining();
if (accumulatorCapacityRemaining != null) {
maxResultsPerBatch = Math.min(maxResultsPerBatch, accumulatorCapacityRemaining + 1);
}
if (maxResultsPerBatch <= 0) {
return false;
}
}
jpaQuery.setMaxResults(maxResultsPerBatch);
jpaQuery.setFirstResult(theQueryIndex * maxResultsPerBatch);
ourLog.debug("Beginning batch expansion for {} with max results per batch: {}", (theAdd ? "inclusion" : "exclusion"), maxResultsPerBatch);
StopWatch swForBatch = new StopWatch();
AtomicInteger countForBatch = new AtomicInteger(0);
List resultList = jpaQuery.getResultList();
int resultsInBatch = resultList.size();
int firstResult = jpaQuery.getFirstResult();
for (Object next : resultList) {
count.incrementAndGet();
countForBatch.incrementAndGet();
TermConcept concept = (TermConcept) next;
try {
addCodeIfNotAlreadyAdded(theValueSetCodeAccumulator, theAddedCodes, concept, theAdd, theCodeCounter);
} catch (ExpansionTooCostlyException e) {
return false;
}
}
ourLog.debug("Batch expansion for {} with starting index of {} produced {} results in {}ms", (theAdd ? "inclusion" : "exclusion"), firstResult, countForBatch, swForBatch.getMillis());
if (resultsInBatch < maxResultsPerBatch) {
ourLog.debug("Expansion for {} produced {} results in {}ms", (theAdd ? "inclusion" : "exclusion"), count, sw.getMillis());
return false;
} else {
return true;
}
return expandValueSetHandleIncludeOrExcludeUsingDatabase(theValueSetCodeAccumulator, theAddedCodes, theIncludeOrExclude, theAdd, theCodeCounter, theQueryIndex, theWantConceptOrNull, system, cs);
} else {
// No CodeSystem matching the URL found in the database.
if (theIncludeOrExclude.getConcept().size() > 0 && theWantConceptOrNull != null) {
if (defaultString(theIncludeOrExclude.getSystem()).equals(theWantConceptOrNull.getSystem())) {
if (theIncludeOrExclude.getConcept().stream().noneMatch(t -> t.getCode().equals(theWantConceptOrNull.getCode()))) {
return false;
}
}
}
// No CodeSystem matching the URL found in the database.
CodeSystem codeSystemFromContext = fetchCanonicalCodeSystemFromCompleteContext(system);
if (codeSystemFromContext == null) {
// This is a last ditch effort.. We don't have a CodeSystem resource for the desired CS, and we don't have
// anything at all in the database that matches it. So let's try asking the validation support context
// just in case there is a registered service that knows how to handle this. This can happen, for example,
// if someone creates a valueset that includes UCUM codes, since we don't have a CodeSystem resource for those
// but CommonCodeSystemsTerminologyService can validate individual codes.
List<VersionIndependentConcept> includedConcepts = null;
if (theWantConceptOrNull != null) {
includedConcepts = new ArrayList<>();
includedConcepts.add(theWantConceptOrNull);
} else if (!theIncludeOrExclude.getConcept().isEmpty()) {
includedConcepts = theIncludeOrExclude
.getConcept()
.stream()
.map(t->new VersionIndependentConcept(theIncludeOrExclude.getSystem(), t.getCode()))
.collect(Collectors.toList());
}
if (includedConcepts != null) {
int foundCount = 0;
for (VersionIndependentConcept next : includedConcepts) {
LookupCodeResult lookup = myValidationSupport.lookupCode(new ValidationSupportContext(myValidationSupport), next.getSystem(), next.getCode());
if (lookup != null && lookup.isFound()) {
addOrRemoveCode(theValueSetCodeAccumulator, theAddedCodes, theAdd, next.getSystem(), next.getCode(), lookup.getCodeDisplay());
foundCount++;
}
}
if (foundCount == includedConcepts.size()) {
return false;
// ELSE, we'll continue below and throw an exception
}
}
String msg = myContext.getLocalizer().getMessage(BaseTermReadSvcImpl.class, "expansionRefersToUnknownCs", system);
if (provideExpansionOptions(theExpansionOptions).isFailOnMissingCodeSystem()) {
throw new PreconditionFailedException(msg);
@ -755,6 +681,7 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc {
theValueSetCodeAccumulator.addMessage(msg);
return false;
}
}
if (!theIncludeOrExclude.getConcept().isEmpty()) {
@ -779,6 +706,7 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc {
return false;
}
} else if (hasValueSet) {
for (CanonicalType nextValueSet : theIncludeOrExclude.getValueSet()) {
@ -825,6 +753,126 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc {
}
@Nonnull
private Boolean expandValueSetHandleIncludeOrExcludeUsingDatabase(IValueSetConceptAccumulator theValueSetCodeAccumulator, Set<String> theAddedCodes, ValueSet.ConceptSetComponent theIncludeOrExclude, boolean theAdd, AtomicInteger theCodeCounter, int theQueryIndex, VersionIndependentConcept theWantConceptOrNull, String theSystem, TermCodeSystem theCs) {
TermCodeSystemVersion csv = theCs.getCurrentVersion();
FullTextEntityManager em = org.hibernate.search.jpa.Search.getFullTextEntityManager(myEntityManager);
/*
* If FullText searching is not enabled, we can handle only basic expansions
* since we're going to do it without the database.
*/
if (myFulltextSearchSvc == null) {
expandWithoutHibernateSearch(theValueSetCodeAccumulator, csv, theAddedCodes, theIncludeOrExclude, theSystem, theAdd, theCodeCounter);
return false;
}
/*
* Ok, let's use hibernate search to build the expansion
*/
QueryBuilder qb = em.getSearchFactory().buildQueryBuilder().forEntity(TermConcept.class).get();
BooleanJunction<?> bool = qb.bool();
bool.must(qb.keyword().onField("myCodeSystemVersionPid").matching(csv.getPid()).createQuery());
if (theWantConceptOrNull != null) {
bool.must(qb.keyword().onField("myCode").matching(theWantConceptOrNull.getCode()).createQuery());
}
/*
* Filters
*/
handleFilters(bool, theSystem, qb, theIncludeOrExclude);
Query luceneQuery = bool.createQuery();
/*
* Include/Exclude Concepts
*/
List<Term> codes = theIncludeOrExclude
.getConcept()
.stream()
.filter(Objects::nonNull)
.map(ValueSet.ConceptReferenceComponent::getCode)
.filter(StringUtils::isNotBlank)
.map(t -> new Term("myCode", t))
.collect(Collectors.toList());
if (codes.size() > 0) {
BooleanQuery.Builder builder = new BooleanQuery.Builder();
builder.setMinimumNumberShouldMatch(1);
for (Term nextCode : codes) {
builder.add(new TermQuery(nextCode), BooleanClause.Occur.SHOULD);
}
luceneQuery = new BooleanQuery.Builder()
.add(luceneQuery, BooleanClause.Occur.MUST)
.add(builder.build(), BooleanClause.Occur.MUST)
.build();
}
/*
* Execute the query
*/
FullTextQuery jpaQuery = em.createFullTextQuery(luceneQuery, TermConcept.class);
/*
* DM 2019-08-21 - Processing slows after any ValueSets with many codes explicitly identified. This might
* be due to the dark arts that is memory management. Will monitor but not do anything about this right now.
*/
BooleanQuery.setMaxClauseCount(10000);
StopWatch sw = new StopWatch();
AtomicInteger count = new AtomicInteger(0);
int maxResultsPerBatch = 10000;
/*
* If the accumulator is bounded, we may reduce the size of the query to
* Lucene in order to be more efficient.
*/
if (theAdd) {
Integer accumulatorCapacityRemaining = theValueSetCodeAccumulator.getCapacityRemaining();
if (accumulatorCapacityRemaining != null) {
maxResultsPerBatch = Math.min(maxResultsPerBatch, accumulatorCapacityRemaining + 1);
}
if (maxResultsPerBatch <= 0) {
return false;
}
}
jpaQuery.setMaxResults(maxResultsPerBatch);
jpaQuery.setFirstResult(theQueryIndex * maxResultsPerBatch);
ourLog.debug("Beginning batch expansion for {} with max results per batch: {}", (theAdd ? "inclusion" : "exclusion"), maxResultsPerBatch);
StopWatch swForBatch = new StopWatch();
AtomicInteger countForBatch = new AtomicInteger(0);
List resultList = jpaQuery.getResultList();
int resultsInBatch = resultList.size();
int firstResult = jpaQuery.getFirstResult();
for (Object next : resultList) {
count.incrementAndGet();
countForBatch.incrementAndGet();
TermConcept concept = (TermConcept) next;
try {
addCodeIfNotAlreadyAdded(theValueSetCodeAccumulator, theAddedCodes, concept, theAdd, theCodeCounter);
} catch (ExpansionTooCostlyException e) {
return false;
}
}
ourLog.debug("Batch expansion for {} with starting index of {} produced {} results in {}ms", (theAdd ? "inclusion" : "exclusion"), firstResult, countForBatch, swForBatch.getMillis());
if (resultsInBatch < maxResultsPerBatch) {
ourLog.debug("Expansion for {} produced {} results in {}ms", (theAdd ? "inclusion" : "exclusion"), count, sw.getMillis());
return false;
} else {
return true;
}
}
private @Nonnull
ValueSetExpansionOptions provideExpansionOptions(@Nullable ValueSetExpansionOptions theExpansionOptions) {
if (theExpansionOptions != null) {
@ -1939,12 +1987,7 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc {
@Override
@Transactional
public CodeValidationResult validateCodeInValueSet(ValidationSupportContext theValidationSupportContext, ConceptValidationOptions theOptions, String theCodeSystem, String theCode, String theDisplay, @Nonnull IBaseResource theValueSet) {
if (myInvokeOnNextCallForUnitTest != null) {
Runnable invokeOnNextCallForUnitTest = myInvokeOnNextCallForUnitTest;
myInvokeOnNextCallForUnitTest = null;
invokeOnNextCallForUnitTest.run();
}
invokeRunnableForUnitTest();
IPrimitiveType<?> urlPrimitive = myContext.newTerser().getSingleValueOrNull(theValueSet, "url", IPrimitiveType.class);
String url = urlPrimitive.getValueAsString();
@ -2115,6 +2158,17 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc {
}
}
/**
* This is only used for unit tests to test failure conditions
*/
static void invokeRunnableForUnitTest() {
if (myInvokeOnNextCallForUnitTest != null) {
Runnable invokeOnNextCallForUnitTest = myInvokeOnNextCallForUnitTest;
myInvokeOnNextCallForUnitTest = null;
invokeOnNextCallForUnitTest.run();
}
}
@VisibleForTesting
public static void setInvokeOnNextCallForUnitTest(Runnable theInvokeOnNextCallForUnitTest) {
myInvokeOnNextCallForUnitTest = theInvokeOnNextCallForUnitTest;

View File

@ -101,6 +101,8 @@ public class TermReadSvcR4 extends BaseTermReadSvcImpl implements ITermReadSvcR4
@CoverageIgnore
@Override
public IValidationSupport.CodeValidationResult validateCode(ValidationSupportContext theValidationSupportContext, ConceptValidationOptions theOptions, String theCodeSystem, String theCode, String theDisplay, String theValueSetUrl) {
invokeRunnableForUnitTest();
Optional<VersionIndependentConcept> codeOpt = Optional.empty();
boolean haveValidated = false;

View File

@ -145,7 +145,8 @@ public class FhirResourceDaoR4QueryCountTest extends BaseJpaR4Test {
// Validate once
myCaptureQueriesListener.clear();
myObservationDao.validate(obs, null, null, null, null, null, null);
assertEquals(9, myCaptureQueriesListener.getSelectQueriesForCurrentThread().size());
myCaptureQueriesListener.logSelectQueriesForCurrentThread();
assertEquals(10, myCaptureQueriesListener.getSelectQueriesForCurrentThread().size());
assertEquals(0, myCaptureQueriesListener.getUpdateQueriesForCurrentThread().size());
assertEquals(0, myCaptureQueriesListener.getInsertQueriesForCurrentThread().size());
assertEquals(0, myCaptureQueriesListener.getDeleteQueriesForCurrentThread().size());

View File

@ -5,6 +5,8 @@ import ca.uhn.fhir.jpa.api.config.DaoConfig;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.entity.TermCodeSystemVersion;
import ca.uhn.fhir.jpa.entity.TermValueSet;
import ca.uhn.fhir.jpa.entity.TermValueSetPreExpansionStatusEnum;
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
import ca.uhn.fhir.jpa.term.BaseTermReadSvcImpl;
import ca.uhn.fhir.jpa.term.api.ITermCodeSystemStorageSvc;
@ -12,6 +14,8 @@ import ca.uhn.fhir.jpa.term.api.ITermReadSvc;
import ca.uhn.fhir.jpa.term.custom.CustomTerminologySet;
import ca.uhn.fhir.jpa.validation.JpaValidationSupportChain;
import ca.uhn.fhir.jpa.validation.ValidationSettings;
import ca.uhn.fhir.parser.LenientErrorHandler;
import ca.uhn.fhir.parser.StrictErrorHandler;
import ca.uhn.fhir.rest.api.EncodingEnum;
import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.rest.api.ValidationModeEnum;
@ -91,6 +95,122 @@ public class FhirResourceDaoR4ValidateTest extends BaseJpaR4Test {
@Autowired
private ValidationSettings myValidationSettings;
/**
* Use a valueset that explicitly brings in some UCUM codes
*/
@Test
public void testValidateCodeInValueSetWithBuiltInCodeSystem() throws IOException {
myValueSetDao.create(loadResourceFromClasspath(ValueSet.class, "/r4/bl/bb-vs.json"));
myStructureDefinitionDao.create(loadResourceFromClasspath(StructureDefinition.class, "/r4/bl/bb-sd.json"));
runInTransaction(() -> {
TermValueSet vs = myTermValueSetDao.findByUrl("https://bb/ValueSet/BBDemographicAgeUnit").orElseThrow(() -> new IllegalArgumentException());
assertEquals(TermValueSetPreExpansionStatusEnum.NOT_EXPANDED, vs.getExpansionStatus());
});
OperationOutcome outcome;
// Use a code that's in the ValueSet
{
outcome = (OperationOutcome) myObservationDao.validate(loadResourceFromClasspath(Observation.class, "/r4/bl/bb-obs-code-in-valueset.json"), null, null, null, null, null, mySrd).getOperationOutcome();
String outcomeStr = myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(outcome);
ourLog.info("Validation outcome: {}", outcomeStr);
assertThat(outcomeStr, not(containsString("\"error\"")));
}
// Use a code that's not in the ValueSet
try {
outcome = (OperationOutcome) myObservationDao.validate(loadResourceFromClasspath(Observation.class, "/r4/bl/bb-obs-code-not-in-valueset.json"), null, null, null, null, null, mySrd).getOperationOutcome();
String outcomeStr = myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(outcome);
ourLog.info("Validation outcome: {}", outcomeStr);
fail();
} catch (PreconditionFailedException e) {
outcome = (OperationOutcome) e.getOperationOutcome();
String outcomeStr = myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(outcome);
ourLog.info("Validation outcome: {}", outcomeStr);
assertThat(outcomeStr, containsString("Could not confirm that the codes provided are in the value set https://bb/ValueSet/BBDemographicAgeUnit, and a code from this value set is required"));
}
// Before, the VS wasn't pre-expanded. Try again with it pre-expanded
runInTransaction(() -> {
TermValueSet vs = myTermValueSetDao.findByUrl("https://bb/ValueSet/BBDemographicAgeUnit").orElseThrow(() -> new IllegalArgumentException());
assertEquals(TermValueSetPreExpansionStatusEnum.NOT_EXPANDED, vs.getExpansionStatus());
});
myTermReadSvc.preExpandDeferredValueSetsToTerminologyTables();
runInTransaction(() -> {
TermValueSet vs = myTermValueSetDao.findByUrl("https://bb/ValueSet/BBDemographicAgeUnit").orElseThrow(() -> new IllegalArgumentException());
assertEquals(TermValueSetPreExpansionStatusEnum.EXPANDED, vs.getExpansionStatus());
});
// Use a code that's in the ValueSet
{
outcome = (OperationOutcome) myObservationDao.validate(loadResourceFromClasspath(Observation.class, "/r4/bl/bb-obs-code-in-valueset.json"), null, null, null, null, null, mySrd).getOperationOutcome();
String outcomeStr = myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(outcome);
ourLog.info("Validation outcome: {}", outcomeStr);
assertThat(outcomeStr, not(containsString("\"error\"")));
}
// Use a code that's not in the ValueSet
try {
outcome = (OperationOutcome) myObservationDao.validate(loadResourceFromClasspath(Observation.class, "/r4/bl/bb-obs-code-not-in-valueset.json"), null, null, null, null, null, mySrd).getOperationOutcome();
String outcomeStr = myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(outcome);
ourLog.info("Validation outcome: {}", outcomeStr);
fail();
} catch (PreconditionFailedException e) {
outcome = (OperationOutcome) e.getOperationOutcome();
String outcomeStr = myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(outcome);
ourLog.info("Validation outcome: {}", outcomeStr);
assertThat(outcomeStr, containsString("Could not confirm that the codes provided are in the value set https://bb/ValueSet/BBDemographicAgeUnit, and a code from this value set is required"));
}
}
@Test
public void testValidateCodeUsingQuantityBinding() throws IOException {
myValueSetDao.create(loadResourceFromClasspath(ValueSet.class, "/r4/bl/bb-vs.json"));
myStructureDefinitionDao.create(loadResourceFromClasspath(StructureDefinition.class, "/r4/bl/bb-sd.json"));
runInTransaction(() -> {
TermValueSet vs = myTermValueSetDao.findByUrl("https://bb/ValueSet/BBDemographicAgeUnit").orElseThrow(() -> new IllegalArgumentException());
assertEquals(TermValueSetPreExpansionStatusEnum.NOT_EXPANDED, vs.getExpansionStatus());
});
OperationOutcome outcome;
// Use the wrong datatype
try {
myFhirCtx.setParserErrorHandler(new LenientErrorHandler());
Observation resource = loadResourceFromClasspath(Observation.class, "/r4/bl/bb-obs-value-is-not-quantity2.json");
outcome = (OperationOutcome) myObservationDao.validate(resource, null, null, null, null, null, mySrd).getOperationOutcome();
String outcomeStr = myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(outcome);
ourLog.info("Validation outcome: {}", outcomeStr);
fail();
} catch (PreconditionFailedException e) {
outcome = (OperationOutcome) e.getOperationOutcome();
String outcomeStr = myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(outcome);
ourLog.info("Validation outcome: {}", outcomeStr);
assertThat(outcomeStr, containsString("\"error\""));
}
// Use the wrong datatype
try {
myFhirCtx.setParserErrorHandler(new LenientErrorHandler());
Observation resource = loadResourceFromClasspath(Observation.class, "/r4/bl/bb-obs-value-is-not-quantity.json");
outcome = (OperationOutcome) myObservationDao.validate(resource, null, null, null, null, null, mySrd).getOperationOutcome();
String outcomeStr = myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(outcome);
ourLog.info("Validation outcome: {}", outcomeStr);
fail();
} catch (PreconditionFailedException e) {
outcome = (OperationOutcome) e.getOperationOutcome();
String outcomeStr = myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(outcome);
ourLog.info("Validation outcome: {}", outcomeStr);
assertThat(outcomeStr, containsString("The Profile \\\"https://bb/StructureDefinition/BBDemographicAge\\\" definition allows for the type Quantity but found type string"));
}
}
/**
* Create a loinc valueset that expands to more results than the expander is willing to do
* in memory, and make sure we can still validate correctly, even if we're using
@ -413,10 +533,16 @@ public class FhirResourceDaoR4ValidateTest extends BaseJpaR4Test {
obs.getCode().getCodingFirstRep().setSystem("http://example.com/codesystem");
obs.getCode().getCodingFirstRep().setCode("foo-foo");
obs.getCode().getCodingFirstRep().setDisplay("Some Code");
outcome = (OperationOutcome) myObservationDao.validate(obs, null, null, null, ValidationModeEnum.CREATE, "http://example.com/structuredefinition", mySrd).getOperationOutcome();
ourLog.info("Outcome: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(outcome));
assertEquals("Unknown code in fragment CodeSystem 'http://example.com/codesystem#foo-foo'", outcome.getIssueFirstRep().getDiagnostics());
assertEquals(OperationOutcome.IssueSeverity.WARNING, outcome.getIssueFirstRep().getSeverity());
try {
outcome = (OperationOutcome) myObservationDao.validate(obs, null, null, null, ValidationModeEnum.CREATE, "http://example.com/structuredefinition", mySrd).getOperationOutcome();
ourLog.info("Outcome: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(outcome));
fail();
} catch (PreconditionFailedException e) {
OperationOutcome oo = (OperationOutcome) e.getOperationOutcome();
ourLog.info(myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(oo));
assertEquals("None of the codes provided are in the value set http://example.com/valueset (http://example.com/valueset), and a code from this value set is required) (codes = http://example.com/codesystem#foo-foo)", oo.getIssueFirstRep().getDiagnostics());
assertEquals(OperationOutcome.IssueSeverity.ERROR, oo.getIssueFirstRep().getSeverity());
}
// Correct codesystem, Code in codesystem
obs.getCode().getCodingFirstRep().setSystem("http://example.com/codesystem");
@ -610,6 +736,12 @@ public class FhirResourceDaoR4ValidateTest extends BaseJpaR4Test {
*/
@Test
public void testValidate_TermSvcHasNpe() {
CodeSystem cs = new CodeSystem();
cs.setUrl("http://FOO");
cs.setContent(CodeSystem.CodeSystemContentMode.NOTPRESENT);
myCodeSystemDao.create(cs);
BaseTermReadSvcImpl.setInvokeOnNextCallForUnitTest(() -> {
throw new NullPointerException("MY ERROR");
});
@ -623,18 +755,15 @@ public class FhirResourceDaoR4ValidateTest extends BaseJpaR4Test {
obs.setStatus(ObservationStatus.FINAL);
obs.setValue(new StringType("This is the value"));
OperationOutcome oo;
// Valid code
obs.getText().setStatus(Narrative.NarrativeStatus.GENERATED);
obs.getCode().getCodingFirstRep().setSystem("http://loinc.org").setCode("CODE99999").setDisplay("Display 3");
try {
validateAndReturnOutcome(obs);
fail();
} catch (NullPointerException e) {
assertEquals("MY ERROR", e.getMessage());
}
obs.getCode().getCodingFirstRep().setSystem("http://FOO").setCode("CODE99999").setDisplay("Display 3");
OperationOutcome oo = validateAndReturnOutcome(obs);
ourLog.info(myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(oo));
assertEquals("Error MY ERROR validating Coding: java.lang.NullPointerException: MY ERROR", oo.getIssueFirstRep().getDiagnostics());
assertEquals(OperationOutcome.IssueSeverity.ERROR, oo.getIssueFirstRep().getSeverity());
}
@Test
@ -943,6 +1072,7 @@ public class FhirResourceDaoR4ValidateTest extends BaseJpaR4Test {
BaseTermReadSvcImpl.setInvokeOnNextCallForUnitTest(null);
myValidationSettings.setLocalReferenceValidationDefaultPolicy(IResourceValidator.ReferenceValidationPolicy.IGNORE);
myFhirCtx.setParserErrorHandler(new StrictErrorHandler());
}
@Test

View File

@ -198,6 +198,25 @@ public class NpmTestR4 extends BaseJpaR4Test {
}
@Test
public void testInstallR4PackageWithNoDescription() throws Exception {
myDaoConfig.setAllowExternalReferences(true);
byte[] bytes = loadClasspathBytes("/packages/UK.Core.r4-1.1.0.tgz");
myFakeNpmServlet.myResponses.put("/UK.Core.r4/1.1.0", bytes);
PackageInstallationSpec spec = new PackageInstallationSpec().setName("UK.Core.r4").setVersion("1.1.0").setInstallMode(PackageInstallationSpec.InstallModeEnum.STORE_AND_INSTALL);
igInstaller.install(spec);
// Be sure no further communication with the server
JettyUtil.closeServer(myServer);
// Make sure we can fetch the package by ID and Version
NpmPackage pkg = myPackageCacheManager.loadPackage("UK.Core.r4", "1.1.0");
assertEquals(null, pkg.description());
assertEquals("UK.Core.r4", pkg.name());
}
@Test
public void testLoadPackageMetadata() throws Exception {
myDaoConfig.setAllowExternalReferences(true);

View File

@ -0,0 +1,22 @@
{
"code": {
"coding": [
{
"code": "L7b8daLEuY",
"system": "https://bbl.health"
}
]
},
"meta": {
"profile": [
"https://bb/StructureDefinition/BBDemographicAge"
]
},
"resourceType": "Observation",
"status": "final",
"valueQuantity" : {
"system" : "http://unitsofmeasure.org",
"code" : "mo",
"value" : 8
}
}

View File

@ -0,0 +1,22 @@
{
"code": {
"coding": [
{
"code": "L7b8daLEuY",
"system": "https://bbl.health"
}
]
},
"meta": {
"profile": [
"https://bb/StructureDefinition/BBDemographicAge"
]
},
"resourceType": "Observation",
"status": "final",
"valueQuantity" : {
"system" : "http://unitsofmeasure.org",
"code" : "cm",
"value" : 8
}
}

View File

@ -0,0 +1,18 @@
{
"meta": {
"profile": [
"https://bb/StructureDefinition/BBDemographicAge"
]
},
"code": {
"coding": [
{
"code": "L7b8daLEuY",
"system": "https://bbl.health"
}
]
},
"resourceType": "Observation",
"status": "final",
"valueString" : "test"
}

View File

@ -0,0 +1,18 @@
{
"meta": {
"profile": [
"https://bb/StructureDefinition/BBDemographicAge"
]
},
"code": {
"coding": [
{
"code": "L7b8daLEuY",
"system": "https://bbl.health"
}
]
},
"resourceType": "Observation",
"status": "final",
"valueDecimal" : 1.23
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,42 @@
{
"resourceType": "ValueSet",
"id": "BBDemographicAgeUnit",
"text": {
"status": "generated",
"div": "<div xmlns=\"http://www.w3.org/1999/xhtml\"></div>"
},
"url": "https://bb/ValueSet/BBDemographicAgeUnit",
"name": "BBDemographicAgeUnit",
"title": "Babylon Demographic Age Unit",
"status": "draft",
"version": "20190731",
"experimental": false,
"description": "Age Unit",
"publisher": "Babylon Partners, Ltd.",
"immutable": false,
"compose": {
"include": [
{
"system": "http://unitsofmeasure.org",
"concept": [
{
"code": "a",
"display": "years"
},
{
"code": "mo",
"display": "months"
},
{
"code": "wk",
"display": "weeks"
},
{
"code": "d",
"display": "days"
}
]
}
]
}
}

View File

@ -38,6 +38,7 @@ import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamUri;
import ca.uhn.fhir.jpa.model.entity.SearchParamPresent;
import ca.uhn.fhir.util.VersionEnum;
import javax.persistence.Column;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
@ -121,6 +122,9 @@ public class HapiFhirJpaMigrationTasks extends BaseMigrationTasks<VersionEnum> {
pkgVerRes.addForeignKey("20200610.12", "FK_NPM_PKVR_RESID").toColumn("BINARY_RES_ID").references("HFJ_RESOURCE", "RES_ID");
pkgVerRes.addIndex("20200610.13", "IDX_PACKVERRES_URL").unique(false).withColumns("CANONICAL_URL");
pkgVerRes.modifyColumn("20200629.1", "PKG_DESC").nullable().withType(ColumnTypeEnum.STRING, 200);
pkgVerRes.modifyColumn("20200629.2", "DESC_UPPER").nullable().withType(ColumnTypeEnum.STRING, 200);
}
private void init501() { //20200514 - present

View File

@ -74,9 +74,9 @@ public class NpmPackageVersionEntity {
@Temporal(TemporalType.TIMESTAMP)
@Column(name = "SAVED_TIME", nullable = false)
private Date mySavedTime;
@Column(name = "PKG_DESC", nullable = false, length = 200)
@Column(name = "PKG_DESC", nullable = true, length = 200)
private String myDescription;
@Column(name = "DESC_UPPER", nullable = false, length = 200)
@Column(name = "DESC_UPPER", nullable = true, length = 200)
private String myDescriptionUpper;
@Column(name = "CURRENT_VERSION", nullable = false)
private boolean myCurrentVersion;

View File

@ -415,6 +415,16 @@ public final class HapiWorkerContext extends I18nBase implements IWorkerContext
throw new UnsupportedOperationException();
}
@Override
public int getClientRetryCount() {
throw new UnsupportedOperationException();
}
@Override
public IWorkerContext setClientRetryCount(int value) {
throw new UnsupportedOperationException();
}
public static ConceptValidationOptions convertConceptValidationOptions(ValidationOptions theOptions) {
ConceptValidationOptions retVal = new ConceptValidationOptions();
if (theOptions.isGuessSystem()) {

View File

@ -23,6 +23,7 @@ import java.util.Map;
import static org.apache.commons.lang3.StringUtils.defaultString;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
/**
* This {@link IValidationSupport validation support module} can be used to validate codes against common
@ -36,8 +37,10 @@ import static org.apache.commons.lang3.StringUtils.isBlank;
public class CommonCodeSystemsTerminologyService implements IValidationSupport {
public static final String LANGUAGES_VALUESET_URL = "http://hl7.org/fhir/ValueSet/languages";
public static final String MIMETYPES_VALUESET_URL = "http://hl7.org/fhir/ValueSet/mimetypes";
public static final String MIMETYPES_CODESYSTEM_URL = "urn:ietf:bcp:13";
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 COUNTRIES_CODESYSTEM_URL = "urn:iso:std:iso:3166";
public static final String UCUM_CODESYSTEM_URL = "http://unitsofmeasure.org";
public static final String UCUM_VALUESET_URL = "http://hl7.org/fhir/ValueSet/ucum-units";
private static final String USPS_CODESYSTEM_URL = "https://www.usps.com/";
@ -45,6 +48,7 @@ public class CommonCodeSystemsTerminologyService implements IValidationSupport {
private static final Logger ourLog = LoggerFactory.getLogger(CommonCodeSystemsTerminologyService.class);
private static Map<String, String> USPS_CODES = Collections.unmodifiableMap(buildUspsCodes());
private static Map<String, String> ISO_4217_CODES = Collections.unmodifiableMap(buildIso4217Codes());
private static Map<String, String> ISO_3166_CODES = Collections.unmodifiableMap(buildIso3166Codes());
private final FhirContext myFhirContext;
/**
@ -144,49 +148,104 @@ public class CommonCodeSystemsTerminologyService implements IValidationSupport {
@Override
public LookupCodeResult lookupCode(ValidationSupportContext theValidationSupportContext, String theSystem, String theCode) {
if (UCUM_CODESYSTEM_URL.equals(theSystem) && theValidationSupportContext.getRootValidationSupport().getFhirContext().getVersion().getVersion().isEqualOrNewerThan(FhirVersionEnum.DSTU3)) {
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) {
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(outcome);
retVal.setCodeDisplay(display);
return retVal;
}
} catch (UcumException e) {
ourLog.debug("Failed parse UCUM code: {}", theCode, e);
break;
case MIMETYPES_CODESYSTEM_URL:
// This is a pretty naive implementation - Should be enhanced in future
LookupCodeResult retVal = new LookupCodeResult();
retVal.setSearchedForCode(theCode);
retVal.setSearchedForSystem(theSystem);
retVal.setFound(true);
return retVal;
default:
return null;
} finally {
ClasspathUtil.close(input);
}
}
return null;
// If we get here it means we know the codesystem but the code was bad
LookupCodeResult retVal = new LookupCodeResult();
retVal.setSearchedForCode(theCode);
retVal.setSearchedForSystem(theSystem);
retVal.setFound(false);
return retVal;
}
@Override
public boolean isCodeSystemSupported(ValidationSupportContext theValidationSupportContext, String theSystem) {
switch (theSystem) {
case COUNTRIES_CODESYSTEM_URL:
case UCUM_CODESYSTEM_URL:
case MIMETYPES_CODESYSTEM_URL:
return true;
}
return false;
}
@Override
public boolean isValueSetSupported(ValidationSupportContext theValidationSupportContext, String theValueSetUrl) {
public String getValueSetUrl(@Nonnull IBaseResource theValueSet) {
switch (theValueSetUrl) {
case CURRENCIES_VALUESET_URL:
case LANGUAGES_VALUESET_URL:
case MIMETYPES_VALUESET_URL:
case UCUM_VALUESET_URL:
case USPS_VALUESET_URL:
return true;
}
return false;
}
@Override
public FhirContext getFhirContext() {
return myFhirContext;
}
public static String getValueSetUrl(@Nonnull IBaseResource theValueSet) {
String url;
switch (getFhirContext().getVersion().getVersion()) {
switch (theValueSet.getStructureFhirVersionEnum()) {
case DSTU2: {
url = ((ca.uhn.fhir.model.dstu2.resource.ValueSet) theValueSet).getUrl();
break;
@ -209,16 +268,11 @@ public class CommonCodeSystemsTerminologyService implements IValidationSupport {
}
case DSTU2_1:
default:
throw new IllegalArgumentException("Can not handle version: " + getFhirContext().getVersion().getVersion());
throw new IllegalArgumentException("Can not handle version: " + theValueSet.getStructureFhirVersionEnum());
}
return url;
}
@Override
public FhirContext getFhirContext() {
return myFhirContext;
}
private static HashMap<String, String> buildUspsCodes() {
HashMap<String, String> uspsCodes = new HashMap<>();
uspsCodes.put("AK", "Alaska");
@ -471,4 +525,259 @@ public class CommonCodeSystemsTerminologyService implements IValidationSupport {
return iso4217Codes;
}
private static HashMap<String, String> buildIso3166Codes() {
HashMap<String, String> codes = new HashMap<>();
codes.put("AF", "Afghanistan");
codes.put("AX", "Åland Islands");
codes.put("AL", "Albania");
codes.put("DZ", "Algeria");
codes.put("AS", "American Samoa");
codes.put("AD", "Andorra");
codes.put("AO", "Angola");
codes.put("AI", "Anguilla");
codes.put("AQ", "Antarctica");
codes.put("AG", "Antigua & Barbuda");
codes.put("AR", "Argentina");
codes.put("AM", "Armenia");
codes.put("AW", "Aruba");
codes.put("AU", "Australia");
codes.put("AT", "Austria");
codes.put("AZ", "Azerbaijan");
codes.put("BS", "Bahamas");
codes.put("BH", "Bahrain");
codes.put("BD", "Bangladesh");
codes.put("BB", "Barbados");
codes.put("BY", "Belarus");
codes.put("BE", "Belgium");
codes.put("BZ", "Belize");
codes.put("BJ", "Benin");
codes.put("BM", "Bermuda");
codes.put("BT", "Bhutan");
codes.put("BO", "Bolivia");
codes.put("BA", "Bosnia & Herzegovina");
codes.put("BW", "Botswana");
codes.put("BV", "Bouvet Island");
codes.put("BR", "Brazil");
codes.put("IO", "British Indian Ocean Territory");
codes.put("VG", "British Virgin Islands");
codes.put("BN", "Brunei");
codes.put("BG", "Bulgaria");
codes.put("BF", "Burkina Faso");
codes.put("BI", "Burundi");
codes.put("KH", "Cambodia");
codes.put("CM", "Cameroon");
codes.put("CA", "Canada");
codes.put("CV", "Cape Verde");
codes.put("BQ", "Caribbean Netherlands");
codes.put("KY", "Cayman Islands");
codes.put("CF", "Central African Republic");
codes.put("TD", "Chad");
codes.put("CL", "Chile");
codes.put("CN", "China");
codes.put("CX", "Christmas Island");
codes.put("CC", "Cocos (Keeling) Islands");
codes.put("CO", "Colombia");
codes.put("KM", "Comoros");
codes.put("CG", "Congo - Brazzaville");
codes.put("CD", "Congo - Kinshasa");
codes.put("CK", "Cook Islands");
codes.put("CR", "Costa Rica");
codes.put("CI", "Côte dIvoire");
codes.put("HR", "Croatia");
codes.put("CU", "Cuba");
codes.put("CW", "Curaçao");
codes.put("CY", "Cyprus");
codes.put("CZ", "Czechia");
codes.put("DK", "Denmark");
codes.put("DJ", "Djibouti");
codes.put("DM", "Dominica");
codes.put("DO", "Dominican Republic");
codes.put("EC", "Ecuador");
codes.put("EG", "Egypt");
codes.put("SV", "El Salvador");
codes.put("GQ", "Equatorial Guinea");
codes.put("ER", "Eritrea");
codes.put("EE", "Estonia");
codes.put("SZ", "Eswatini");
codes.put("ET", "Ethiopia");
codes.put("FK", "Falkland Islands");
codes.put("FO", "Faroe Islands");
codes.put("FJ", "Fiji");
codes.put("FI", "Finland");
codes.put("FR", "France");
codes.put("GF", "French Guiana");
codes.put("PF", "French Polynesia");
codes.put("TF", "French Southern Territories");
codes.put("GA", "Gabon");
codes.put("GM", "Gambia");
codes.put("GE", "Georgia");
codes.put("DE", "Germany");
codes.put("GH", "Ghana");
codes.put("GI", "Gibraltar");
codes.put("GR", "Greece");
codes.put("GL", "Greenland");
codes.put("GD", "Grenada");
codes.put("GP", "Guadeloupe");
codes.put("GU", "Guam");
codes.put("GT", "Guatemala");
codes.put("GG", "Guernsey");
codes.put("GN", "Guinea");
codes.put("GW", "Guinea-Bissau");
codes.put("GY", "Guyana");
codes.put("HT", "Haiti");
codes.put("HM", "Heard & McDonald Islands");
codes.put("HN", "Honduras");
codes.put("HK", "Hong Kong SAR China");
codes.put("HU", "Hungary");
codes.put("IS", "Iceland");
codes.put("IN", "India");
codes.put("ID", "Indonesia");
codes.put("IR", "Iran");
codes.put("IQ", "Iraq");
codes.put("IE", "Ireland");
codes.put("IM", "Isle of Man");
codes.put("IL", "Israel");
codes.put("IT", "Italy");
codes.put("JM", "Jamaica");
codes.put("JP", "Japan");
codes.put("JE", "Jersey");
codes.put("JO", "Jordan");
codes.put("KZ", "Kazakhstan");
codes.put("KE", "Kenya");
codes.put("KI", "Kiribati");
codes.put("KW", "Kuwait");
codes.put("KG", "Kyrgyzstan");
codes.put("LA", "Laos");
codes.put("LV", "Latvia");
codes.put("LB", "Lebanon");
codes.put("LS", "Lesotho");
codes.put("LR", "Liberia");
codes.put("LY", "Libya");
codes.put("LI", "Liechtenstein");
codes.put("LT", "Lithuania");
codes.put("LU", "Luxembourg");
codes.put("MO", "Macao SAR China");
codes.put("MG", "Madagascar");
codes.put("MW", "Malawi");
codes.put("MY", "Malaysia");
codes.put("MV", "Maldives");
codes.put("ML", "Mali");
codes.put("MT", "Malta");
codes.put("MH", "Marshall Islands");
codes.put("MQ", "Martinique");
codes.put("MR", "Mauritania");
codes.put("MU", "Mauritius");
codes.put("YT", "Mayotte");
codes.put("MX", "Mexico");
codes.put("FM", "Micronesia");
codes.put("MD", "Moldova");
codes.put("MC", "Monaco");
codes.put("MN", "Mongolia");
codes.put("ME", "Montenegro");
codes.put("MS", "Montserrat");
codes.put("MA", "Morocco");
codes.put("MZ", "Mozambique");
codes.put("MM", "Myanmar (Burma)");
codes.put("NA", "Namibia");
codes.put("NR", "Nauru");
codes.put("NP", "Nepal");
codes.put("NL", "Netherlands");
codes.put("NC", "New Caledonia");
codes.put("NZ", "New Zealand");
codes.put("NI", "Nicaragua");
codes.put("NE", "Niger");
codes.put("NG", "Nigeria");
codes.put("NU", "Niue");
codes.put("NF", "Norfolk Island");
codes.put("KP", "North Korea");
codes.put("MK", "North Macedonia");
codes.put("MP", "Northern Mariana Islands");
codes.put("NO", "Norway");
codes.put("OM", "Oman");
codes.put("PK", "Pakistan");
codes.put("PW", "Palau");
codes.put("PS", "Palestinian Territories");
codes.put("PA", "Panama");
codes.put("PG", "Papua New Guinea");
codes.put("PY", "Paraguay");
codes.put("PE", "Peru");
codes.put("PH", "Philippines");
codes.put("PN", "Pitcairn Islands");
codes.put("PL", "Poland");
codes.put("PT", "Portugal");
codes.put("PR", "Puerto Rico");
codes.put("QA", "Qatar");
codes.put("RE", "Réunion");
codes.put("RO", "Romania");
codes.put("RU", "Russia");
codes.put("RW", "Rwanda");
codes.put("WS", "Samoa");
codes.put("SM", "San Marino");
codes.put("ST", "São Tomé & Príncipe");
codes.put("SA", "Saudi Arabia");
codes.put("SN", "Senegal");
codes.put("RS", "Serbia");
codes.put("SC", "Seychelles");
codes.put("SL", "Sierra Leone");
codes.put("SG", "Singapore");
codes.put("SX", "Sint Maarten");
codes.put("SK", "Slovakia");
codes.put("SI", "Slovenia");
codes.put("SB", "Solomon Islands");
codes.put("SO", "Somalia");
codes.put("ZA", "South Africa");
codes.put("GS", "South Georgia & South Sandwich Islands");
codes.put("KR", "South Korea");
codes.put("SS", "South Sudan");
codes.put("ES", "Spain");
codes.put("LK", "Sri Lanka");
codes.put("BL", "St. Barthélemy");
codes.put("SH", "St. Helena");
codes.put("KN", "St. Kitts & Nevis");
codes.put("LC", "St. Lucia");
codes.put("MF", "St. Martin");
codes.put("PM", "St. Pierre & Miquelon");
codes.put("VC", "St. Vincent & Grenadines");
codes.put("SD", "Sudan");
codes.put("SR", "Suriname");
codes.put("SJ", "Svalbard & Jan Mayen");
codes.put("SE", "Sweden");
codes.put("CH", "Switzerland");
codes.put("SY", "Syria");
codes.put("TW", "Taiwan");
codes.put("TJ", "Tajikistan");
codes.put("TZ", "Tanzania");
codes.put("TH", "Thailand");
codes.put("TL", "Timor-Leste");
codes.put("TG", "Togo");
codes.put("TK", "Tokelau");
codes.put("TO", "Tonga");
codes.put("TT", "Trinidad & Tobago");
codes.put("TN", "Tunisia");
codes.put("TR", "Turkey");
codes.put("TM", "Turkmenistan");
codes.put("TC", "Turks & Caicos Islands");
codes.put("TV", "Tuvalu");
codes.put("UM", "U.S. Outlying Islands");
codes.put("VI", "U.S. Virgin Islands");
codes.put("UG", "Uganda");
codes.put("UA", "Ukraine");
codes.put("AE", "United Arab Emirates");
codes.put("GB", "United Kingdom");
codes.put("US", "United States");
codes.put("UY", "Uruguay");
codes.put("UZ", "Uzbekistan");
codes.put("VU", "Vanuatu");
codes.put("VA", "Vatican City");
codes.put("VE", "Venezuela");
codes.put("VN", "Vietnam");
codes.put("WF", "Wallis & Futuna");
codes.put("EH", "Western Sahara");
codes.put("YE", "Yemen");
codes.put("ZM", "Zambia");
codes.put("ZW", "Zimbabwe");
return codes;
}
}

View File

@ -119,7 +119,8 @@ public class InMemoryTerminologyServerValidationSupport implements IValidationSu
}
@Override
public CodeValidationResult validateCodeInValueSet(ValidationSupportContext theValidationSupportContext, ConceptValidationOptions theOptions, String theCodeSystem, String theCode, String theDisplay, @Nonnull IBaseResource theValueSet) {
public CodeValidationResult
validateCodeInValueSet(ValidationSupportContext theValidationSupportContext, ConceptValidationOptions theOptions, String theCodeSystem, String theCode, String theDisplay, @Nonnull IBaseResource theValueSet) {
org.hl7.fhir.r5.model.ValueSet expansion = expandValueSetToCanonical(theValidationSupportContext, theValueSet, theCodeSystem, theCode);
if (expansion == null) {
return null;
@ -439,39 +440,33 @@ public class InMemoryTerminologyServerValidationSupport implements IValidationSu
}
boolean ableToHandleCode = false;
if (codeSystem == null) {
if (codeSystem == null || codeSystem.getContent() == CodeSystem.CodeSystemContentMode.NOTPRESENT) {
if (theWantCode != null) {
LookupCodeResult lookup = theValidationSupportContext.getRootValidationSupport().lookupCode(theValidationSupportContext, 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;
if (theValidationSupportContext.getRootValidationSupport().isCodeSystemSupported(theValidationSupportContext, system)) {
LookupCodeResult lookup = theValidationSupportContext.getRootValidationSupport().lookupCode(theValidationSupportContext, 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();
}
if (codeSystem != null && codeSystem.getContent() != CodeSystem.CodeSystemContentMode.NOTPRESENT) {
addCodes(system, codeSystem.getConcept(), nextCodeList, wantCodes);
}
}

View File

@ -231,10 +231,12 @@ public class ValidationSupportChain implements IValidationSupport {
@Override
public CodeValidationResult validateCode(ValidationSupportContext theValidationSupportContext, ConceptValidationOptions theOptions, String theCodeSystem, String theCode, String theDisplay, String theValueSetUrl) {
for (IValidationSupport next : myChain) {
if (theOptions.isInferSystem() || (theCodeSystem != null && next.isCodeSystemSupported(theValidationSupportContext, theCodeSystem))) {
CodeValidationResult retVal = next.validateCode(theValidationSupportContext, theOptions, theCodeSystem, theCode, theDisplay, theValueSetUrl);
if (retVal != null) {
return retVal;
if (isBlank(theValueSetUrl) || next.isValueSetSupported(theValidationSupportContext, theValueSetUrl)) {
if (theOptions.isInferSystem() || (theCodeSystem != null && next.isCodeSystemSupported(theValidationSupportContext, theCodeSystem))) {
CodeValidationResult retVal = next.validateCode(theValidationSupportContext, theOptions, theCodeSystem, theCode, theDisplay, theValueSetUrl);
if (retVal != null) {
return retVal;
}
}
}
}
@ -244,7 +246,8 @@ public class ValidationSupportChain implements IValidationSupport {
@Override
public CodeValidationResult validateCodeInValueSet(ValidationSupportContext theValidationSupportContext, ConceptValidationOptions theOptions, String theCodeSystem, String theCode, String theDisplay, @Nonnull IBaseResource theValueSet) {
for (IValidationSupport next : myChain) {
if (theOptions.isInferSystem() || (theCodeSystem != null && next.isCodeSystemSupported(theValidationSupportContext, theCodeSystem))) {
String url = CommonCodeSystemsTerminologyService.getValueSetUrl(theValueSet);
if (isBlank(url) || next.isValueSetSupported(theValidationSupportContext, url)) {
CodeValidationResult retVal = next.validateCodeInValueSet(theValidationSupportContext, theOptions, theCodeSystem, theCode, theDisplay, theValueSet);
if (retVal != null) {
return retVal;

View File

@ -131,6 +131,16 @@ public class VersionSpecificWorkerContextWrapper extends I18nBase implements IWo
return false;
}
@Override
public int getClientRetryCount() {
throw new UnsupportedOperationException();
}
@Override
public IWorkerContext setClientRetryCount(int value) {
throw new UnsupportedOperationException();
}
@Override
public void generateSnapshot(StructureDefinition input) throws FHIRException {
if (input.hasSnapshot()) {

View File

@ -167,6 +167,18 @@ public class FhirInstanceValidatorDstu3Test {
return retVal;
}
});
// when(mockSupport.isValueSetSupported(any(), nullable(String.class))).thenAnswer(new Answer<Boolean>() {
// @Override
// public Boolean answer(InvocationOnMock theInvocation) {
// String url = (String) theInvocation.getArguments()[1];
// boolean retVal = myValueSets.containsKey(url);
// return retVal;
// }
// });
when(mockSupport.fetchValueSet(any())).thenAnswer(t->{
String url = t.getArgument(0, String.class);
return myValueSets.get(url);
});
when(mockSupport.fetchResource(nullable(Class.class), nullable(String.class))).thenAnswer(new Answer<IBaseResource>() {
@Override
public IBaseResource answer(InvocationOnMock theInvocation) throws Throwable {
@ -212,7 +224,7 @@ public class FhirInstanceValidatorDstu3Test {
if (myValidConcepts.contains(system + "___" + code)) {
retVal = new IValidationSupport.CodeValidationResult().setCode(code);
} else if (myValidSystems.contains(system)) {
return new IValidationSupport.CodeValidationResult().setSeverityCode(ValidationMessage.IssueSeverity.WARNING.toCode()).setMessage("Unknown code: " + system + " / " + code);
return new IValidationSupport.CodeValidationResult().setSeverityCode(ValidationMessage.IssueSeverity.ERROR.toCode()).setMessage("Unknown code");
} else if (myCodeSystems.containsKey(system)) {
CodeSystem cs = myCodeSystems.get(system);
Optional<ConceptDefinitionComponent> found = cs.getConcept().stream().filter(t -> t.getCode().equals(code)).findFirst();
@ -1035,8 +1047,8 @@ public class FhirInstanceValidatorDstu3Test {
ValidationResult output = myVal.validateWithResult(input);
List<SingleValidationMessage> errors = logResultsAndReturnAll(output);
assertThat(errors.toString(), containsString("warning"));
assertThat(errors.toString(), containsString("Unknown code: http://loinc.org / 12345"));
assertEquals(ResultSeverityEnum.ERROR, errors.get(0).getSeverity());
assertEquals("Unknown code for \"http://loinc.org#12345\"", errors.get(0).getMessage());
}
@Test
@ -1058,7 +1070,6 @@ public class FhirInstanceValidatorDstu3Test {
assertThat(errors.toString(), containsString("Element 'Observation.subject': minimum required = 1, but only found 0"));
assertThat(errors.toString(), containsString("Element 'Observation.context': max allowed = 0, but found 1"));
assertThat(errors.toString(), containsString("Element 'Observation.device': minimum required = 1, but only found 0"));
assertThat(errors.toString(), containsString(""));
}
@Test
@ -1150,7 +1161,7 @@ public class FhirInstanceValidatorDstu3Test {
ValidationResult output = myVal.validateWithResult(input);
List<SingleValidationMessage> errors = logResultsAndReturnAll(output);
assertThat(errors.toString(), errors.size(), greaterThan(0));
assertEquals("Unknown code: http://acme.org / 9988877", errors.get(0).getMessage());
assertEquals("Unknown code for \"http://acme.org#9988877\"", errors.get(0).getMessage());
}
@ -1186,7 +1197,7 @@ public class FhirInstanceValidatorDstu3Test {
ValidationResult output = myVal.validateWithResult(input);
List<SingleValidationMessage> errors = logResultsAndReturnNonInformationalOnes(output);
assertEquals(1, errors.size());
assertEquals("Unknown code: http://loinc.org / 1234", errors.get(0).getMessage());
assertEquals("Unknown code for \"http://loinc.org#1234\"", errors.get(0).getMessage());
}
@Test

View File

@ -1038,6 +1038,8 @@ public class QuestionnaireResponseValidatorDstu3Test {
options.getCompose().addInclude().setSystem("http://codesystems.com/system2").addConcept().setCode("code2");
when(myValSupport.fetchResource(eq(ValueSet.class), eq("http://somevalueset"))).thenReturn(options);
when(myValSupport.isValueSetSupported(any(), eq("http://somevalueset"))).thenReturn(true);
when(myValSupport.validateCodeInValueSet(any(), any(), eq("http://codesystems.com/system"), eq("code0"), any(), any(IBaseResource.class))).thenReturn(new IValidationSupport.CodeValidationResult().setCode("code0"));
QuestionnaireResponse qa;

View File

@ -206,7 +206,7 @@ public class FhirInstanceValidatorR4Test extends BaseTest {
if (myValidConcepts.contains(system + "___" + code)) {
retVal = new IValidationSupport.CodeValidationResult().setCode(code);
} else if (myValidSystems.contains(system)) {
return new IValidationSupport.CodeValidationResult().setSeverityCode(ValidationMessage.IssueSeverity.WARNING.toCode()).setMessage("Unknown code: " + system + " / " + code);
return new IValidationSupport.CodeValidationResult().setSeverityCode(ValidationMessage.IssueSeverity.ERROR.toCode()).setMessage("Unknown code");
} else {
retVal = myDefaultValidationSupport.validateCode(new ValidationSupportContext(myDefaultValidationSupport), options, system, code, display, valueSetUrl);
}
@ -1110,8 +1110,8 @@ public class FhirInstanceValidatorR4Test extends BaseTest {
ValidationResult output = myVal.validateWithResult(input);
List<SingleValidationMessage> errors = logResultsAndReturnAll(output);
assertThat(errors.toString(), containsString("warning"));
assertThat(errors.toString(), containsString("Unknown code: http://loinc.org / 12345"));
assertEquals(ResultSeverityEnum.ERROR, errors.get(0).getSeverity());
assertEquals("Unknown code for \"http://loinc.org#12345\"", errors.get(0).getMessage());
}
@Test
@ -1266,7 +1266,7 @@ public class FhirInstanceValidatorR4Test extends BaseTest {
ValidationResult output = myVal.validateWithResult(input);
List<SingleValidationMessage> errors = logResultsAndReturnAll(output);
assertThat(errors.toString(), errors.size(), greaterThan(0));
assertEquals("Unknown code: http://acme.org / 9988877", errors.get(0).getMessage());
assertEquals("Unknown code for \"http://acme.org#9988877\"", errors.get(0).getMessage());
}
@ -1304,7 +1304,7 @@ public class FhirInstanceValidatorR4Test extends BaseTest {
ValidationResult output = myVal.validateWithResult(input);
List<SingleValidationMessage> errors = logResultsAndReturnNonInformationalOnes(output);
assertEquals(1, errors.size());
assertEquals("Unknown code: http://loinc.org / 1234", errors.get(0).getMessage());
assertEquals("Unknown code for \"http://loinc.org#1234\"", errors.get(0).getMessage());
}
@Test

View File

@ -9,6 +9,7 @@ import ca.uhn.fhir.validation.FhirValidator;
import ca.uhn.fhir.validation.ResultSeverityEnum;
import ca.uhn.fhir.validation.SingleValidationMessage;
import ca.uhn.fhir.validation.ValidationResult;
import org.hl7.fhir.common.hapi.validation.support.CommonCodeSystemsTerminologyService;
import org.hl7.fhir.common.hapi.validation.support.InMemoryTerminologyServerValidationSupport;
import org.hl7.fhir.common.hapi.validation.support.ValidationSupportChain;
import org.hl7.fhir.common.hapi.validation.validator.FhirInstanceValidator;
@ -61,7 +62,7 @@ public class QuestionnaireResponseValidatorR4Test {
myVal.setValidateAgainstStandardSchema(false);
myVal.setValidateAgainstStandardSchematron(false);
ValidationSupportChain validationSupport = new ValidationSupportChain(myDefaultValidationSupport, myValSupport, new InMemoryTerminologyServerValidationSupport(ourCtx));
ValidationSupportChain validationSupport = new ValidationSupportChain(myDefaultValidationSupport, myValSupport, new InMemoryTerminologyServerValidationSupport(ourCtx), new CommonCodeSystemsTerminologyService(ourCtx));
myInstanceVal = new FhirInstanceValidator(validationSupport);
myVal.registerValidatorModule(myInstanceVal);

View File

@ -163,7 +163,7 @@ public class FhirInstanceValidatorR5Test {
if (myValidConcepts.contains(system + "___" + code)) {
retVal = new IValidationSupport.CodeValidationResult().setCode(code);
} else if (myValidSystems.contains(system)) {
return new IValidationSupport.CodeValidationResult().setSeverity(IValidationSupport.IssueSeverity.WARNING).setMessage("Unknown code: " + system + " / " + code);
return new IValidationSupport.CodeValidationResult().setSeverity(IValidationSupport.IssueSeverity.ERROR).setMessage("Unknown code");
} else {
retVal = myDefaultValidationSupport.validateCode(new ValidationSupportContext(myDefaultValidationSupport), options, system, code, display, valueSetUrl);
}
@ -754,8 +754,8 @@ public class FhirInstanceValidatorR5Test {
ValidationResult output = myVal.validateWithResult(input);
List<SingleValidationMessage> errors = logResultsAndReturnAll(output);
assertThat(errors.toString(), containsString("warning"));
assertThat(errors.toString(), containsString("Unknown code: http://loinc.org / 12345"));
assertEquals(ResultSeverityEnum.ERROR, errors.get(0).getSeverity());
assertEquals("Unknown code for \"http://loinc.org#12345\"", errors.get(0).getMessage());
}
@Test
@ -877,7 +877,7 @@ public class FhirInstanceValidatorR5Test {
ValidationResult output = myVal.validateWithResult(input);
List<SingleValidationMessage> errors = logResultsAndReturnAll(output);
assertThat(errors.toString(), errors.size(), greaterThan(0));
assertEquals("Unknown code: http://acme.org / 9988877", errors.get(0).getMessage());
assertEquals("Unknown code for \"http://acme.org#9988877\"", errors.get(0).getMessage());
}
@ -915,7 +915,7 @@ public class FhirInstanceValidatorR5Test {
ValidationResult output = myVal.validateWithResult(input);
List<SingleValidationMessage> errors = logResultsAndReturnNonInformationalOnes(output);
assertEquals(1, errors.size());
assertEquals("Unknown code: http://loinc.org / 1234", errors.get(0).getMessage());
assertEquals("Unknown code for \"http://loinc.org#1234\"", errors.get(0).getMessage());
}
@Test

View File

@ -9,6 +9,7 @@ import ca.uhn.fhir.validation.FhirValidator;
import ca.uhn.fhir.validation.ResultSeverityEnum;
import ca.uhn.fhir.validation.SingleValidationMessage;
import ca.uhn.fhir.validation.ValidationResult;
import org.hl7.fhir.common.hapi.validation.support.CommonCodeSystemsTerminologyService;
import org.hl7.fhir.common.hapi.validation.support.InMemoryTerminologyServerValidationSupport;
import org.hl7.fhir.common.hapi.validation.support.ValidationSupportChain;
import org.hl7.fhir.common.hapi.validation.validator.FhirInstanceValidator;
@ -65,7 +66,7 @@ public class QuestionnaireResponseValidatorR5Test {
myVal.setValidateAgainstStandardSchema(false);
myVal.setValidateAgainstStandardSchematron(false);
ValidationSupportChain validationSupport = new ValidationSupportChain(myDefaultValidationSupport, myValSupport, new InMemoryTerminologyServerValidationSupport(ourCtx));
ValidationSupportChain validationSupport = new ValidationSupportChain(myDefaultValidationSupport, myValSupport, new InMemoryTerminologyServerValidationSupport(ourCtx), new CommonCodeSystemsTerminologyService(ourCtx));
myInstanceVal = new FhirInstanceValidator(validationSupport);
myVal.registerValidatorModule(myInstanceVal);

View File

@ -674,7 +674,7 @@
<properties>
<fhir_core_version>5.0.7-SNAPSHOT</fhir_core_version>
<fhir_core_version>5.0.9</fhir_core_version>
<ucum_version>1.0.2</ucum_version>
<surefire_jvm_args>-Dfile.encoding=UTF-8 -Xmx2048m</surefire_jvm_args>