Improve ValueSet filtering (#2162)

* Improve filter search

* Filter improvements

* Tests passing

* Test fixes

* Fix transaction filter

* Add changelog

* Test fix

* Update hapi-fhir-base/src/main/resources/ca/uhn/fhir/i18n/hapi-messages.properties

Co-authored-by: Diederik Muylwyk <diederik.muylwyk@gmail.com>

* Update hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/BaseTermReadSvcImpl.java

Co-authored-by: Diederik Muylwyk <diederik.muylwyk@gmail.com>

* Resolve FIXME

* Test fix

* Update hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/term/ValueSetExpansionR4Test.java

Co-authored-by: Diederik Muylwyk <diederik.muylwyk@gmail.com>

Co-authored-by: Diederik Muylwyk <diederik.muylwyk@gmail.com>
This commit is contained in:
James Agnew 2020-11-10 11:01:13 -05:00 committed by GitHub
parent 4f6be9d9a7
commit 63b7b379c2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 777 additions and 284 deletions

View File

@ -1,5 +1,8 @@
ca.uhn.fhir.jpa.term.BaseTermReadSvcImpl.expansionRefersToUnknownCs=Unknown CodeSystem URI "{0}" referenced from ValueSet
ca.uhn.fhir.jpa.term.BaseTermReadSvcImpl.valueSetNotYetExpanded=ValueSet "{0}" has not yet been pre-expanded. Performing in-memory expansion without parameters. Current status: {1} | {2}
ca.uhn.fhir.jpa.term.BaseTermReadSvcImpl.valueSetNotYetExpanded_OffsetNotAllowed=ValueSet expansion can not combine "offset" with "ValueSet.compose.exclude" unless the ValueSet has been pre-expanded. ValueSet "{0}" must be pre-expanded for this operation to work.
# Core Library Messages
ca.uhn.fhir.context.FhirContext.unknownResourceName=Unknown resource name "{0}" (this name is not known in FHIR version "{1}")

View File

@ -0,0 +1,5 @@
---
type: fix
issue: 2162
title: "When expanding a pre-expanded ValueSet using a filter, the filter was ignored and the pre-expansion was not used
resulting in an inefficient and potentially incorrect expansion. This has been corrected."

View File

@ -1,6 +1,8 @@
package ca.uhn.fhir.jpa.dao.data;
import ca.uhn.fhir.jpa.entity.TermValueSetConceptView;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
@ -32,4 +34,7 @@ public interface ITermValueSetConceptViewDao extends JpaRepository<TermValueSetC
@Query("SELECT v FROM TermValueSetConceptView v WHERE v.myConceptValueSetPid = :pid AND v.myConceptOrder >= :from AND v.myConceptOrder < :to ORDER BY v.myConceptOrder")
List<TermValueSetConceptView> findByTermValueSetId(@Param("from") int theFrom, @Param("to") int theTo, @Param("pid") Long theValueSetId);
@Query("SELECT v FROM TermValueSetConceptView v WHERE v.myConceptValueSetPid = :pid AND v.myConceptDisplay LIKE :display ORDER BY v.myConceptOrder")
List<TermValueSetConceptView> findByTermValueSetId(@Param("pid") Long theValueSetId, @Param("display") String theDisplay);
}

View File

@ -27,6 +27,7 @@ import ca.uhn.fhir.jpa.api.dao.IFhirResourceDaoValueSet;
import ca.uhn.fhir.jpa.dao.BaseHapiFhirResourceDao;
import ca.uhn.fhir.jpa.model.cross.IBasePersistedResource;
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
import ca.uhn.fhir.jpa.model.util.JpaConstants;
import ca.uhn.fhir.rest.api.server.storage.TransactionDetails;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
@ -221,7 +222,7 @@ public class FhirResourceDaoValueSetR4 extends BaseHapiFhirResourceDao<ValueSet>
private void addFilterIfPresent(String theFilter, ConceptSetComponent include) {
if (ElementUtil.isEmpty(include.getConcept())) {
if (isNotBlank(theFilter)) {
include.addFilter().setProperty("display").setOp(FilterOperator.EQUAL).setValue(theFilter);
include.addFilter().setProperty(JpaConstants.VALUESET_FILTER_DISPLAY).setOp(FilterOperator.EQUAL).setValue(theFilter);
}
}
}

View File

@ -94,7 +94,6 @@ public class HapiTransactionService {
}
}
}
/**

View File

@ -37,8 +37,6 @@ import ca.uhn.fhir.jpa.entity.Search;
import ca.uhn.fhir.jpa.entity.SearchInclude;
import ca.uhn.fhir.jpa.entity.SearchTypeEnum;
import ca.uhn.fhir.jpa.interceptor.JpaPreResourceAccessDetails;
import ca.uhn.fhir.model.api.IQueryParameterType;
import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId;
import ca.uhn.fhir.jpa.model.search.SearchRuntimeDetails;
import ca.uhn.fhir.jpa.model.search.SearchStatusEnum;
import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc;
@ -47,6 +45,7 @@ import ca.uhn.fhir.jpa.search.cache.ISearchResultCacheSvc;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.jpa.util.InterceptorUtil;
import ca.uhn.fhir.jpa.util.JpaInterceptorBroadcaster;
import ca.uhn.fhir.model.api.IQueryParameterType;
import ca.uhn.fhir.model.api.Include;
import ca.uhn.fhir.rest.api.CacheControlDirective;
import ca.uhn.fhir.rest.api.Constants;
@ -468,7 +467,7 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
TransactionTemplate txTemplate = new TransactionTemplate(myManagedTxManager);
txTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
return txTemplate.execute(t -> {
// Load the results synchronously
final List<ResourcePersistentId> pids = new ArrayList<>();
@ -668,6 +667,12 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
myRequestPartitionHelperService = theRequestPartitionHelperService;
}
private boolean isWantCount(SearchParameterMap myParams, boolean wantOnlyCount) {
return wantOnlyCount ||
SearchTotalModeEnum.ACCURATE.equals(myParams.getSearchTotalMode()) ||
(myParams.getSearchTotalMode() == null && SearchTotalModeEnum.ACCURATE.equals(myDaoConfig.getDefaultTotalMode()));
}
/**
* A search task is a Callable task that runs in
* a thread pool to handle an individual search. One instance
@ -691,6 +696,8 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
private final ArrayList<ResourcePersistentId> myUnsyncedPids = new ArrayList<>();
private final RequestDetails myRequest;
private final RequestPartitionId myRequestPartitionId;
private final SearchRuntimeDetails mySearchRuntimeDetails;
private final Transaction myParentTransaction;
private Search mySearch;
private boolean myAbortRequested;
private int myCountSavedTotal = 0;
@ -699,8 +706,6 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
private boolean myAdditionalPrefetchThresholdsRemaining;
private List<ResourcePersistentId> myPreviouslyAddedResourcePids;
private Integer myMaxResultsToFetch;
private final SearchRuntimeDetails mySearchRuntimeDetails;
private final Transaction myParentTransaction;
/**
* Constructor
@ -1193,17 +1198,6 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
}
}
private boolean isWantCount(SearchParameterMap myParams, boolean wantOnlyCount) {
return wantOnlyCount ||
SearchTotalModeEnum.ACCURATE.equals(myParams.getSearchTotalMode()) ||
(myParams.getSearchTotalMode() == null && SearchTotalModeEnum.ACCURATE.equals(myDaoConfig.getDefaultTotalMode()));
}
private static boolean isWantOnlyCount(SearchParameterMap myParams) {
return SummaryEnum.COUNT.equals(myParams.getSummaryMode())
| INTEGER_0.equals(myParams.getCount());
}
public class SearchContinuationTask extends SearchTask {
public SearchContinuationTask(Search theSearch, IDao theCallingDao, SearchParameterMap theParams, String theResourceType, RequestDetails theRequest, RequestPartitionId theRequestPartitionId) {
@ -1242,6 +1236,10 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
}
private static boolean isWantOnlyCount(SearchParameterMap myParams) {
return SummaryEnum.COUNT.equals(myParams.getSummaryMode())
| INTEGER_0.equals(myParams.getCount());
}
public static void populateSearchEntity(SearchParameterMap theParams, String theResourceType, String theSearchUuid, String theQueryString, Search theSearch) {
theSearch.setDeleted(false);
@ -1270,8 +1268,8 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
* Creates a {@link Pageable} using a start and end index
*/
@SuppressWarnings("WeakerAccess")
public static @Nullable
Pageable toPage(final int theFromIndex, int theToIndex) {
@Nullable
public static Pageable toPage(final int theFromIndex, int theToIndex) {
int pageSize = theToIndex - theFromIndex;
if (pageSize < 1) {
return null;

View File

@ -77,6 +77,7 @@ public class UriPredicateBuilder extends BaseSearchParamPredicateBuilder {
public Condition addPredicate(List<? extends IQueryParameterType> theUriOrParameterList, String theParamName, SearchFilterParser.CompareOperation theOperation, RequestDetails theRequestDetails) {
List<Condition> codePredicates = new ArrayList<>();
boolean predicateIsHash = false;
for (IQueryParameterType nextOr : theUriOrParameterList) {
if (nextOr instanceof UriParam) {
@ -141,8 +142,8 @@ public class UriPredicateBuilder extends BaseSearchParamPredicateBuilder {
Condition uriPredicate = null;
if (theOperation == null || theOperation == SearchFilterParser.CompareOperation.eq) {
long hashUri = ResourceIndexedSearchParamUri.calculateHashUri(getPartitionSettings(), getRequestPartitionId(), getResourceType(), theParamName, value);
Condition hashPredicate = BinaryCondition.equalTo(myColumnHashUri, generatePlaceholder(hashUri));
codePredicates.add(hashPredicate);
uriPredicate = BinaryCondition.equalTo(myColumnHashUri, generatePlaceholder(hashUri));
predicateIsHash = true;
} else if (theOperation == SearchFilterParser.CompareOperation.ne) {
uriPredicate = BinaryCondition.notEqualTo(myColumnUri, generatePlaceholder(value));
} else if (theOperation == SearchFilterParser.CompareOperation.co) {
@ -164,11 +165,7 @@ public class UriPredicateBuilder extends BaseSearchParamPredicateBuilder {
theOperation.toString()));
}
if (uriPredicate != null) {
long hashIdentity = BaseResourceIndexedSearchParam.calculateHashIdentity(getPartitionSettings(), getRequestPartitionId(), getResourceType(), theParamName);
BinaryCondition hashIdentityPredicate = BinaryCondition.equalTo(getColumnHashIdentity(), generatePlaceholder(hashIdentity));
codePredicates.add(ComboCondition.and(hashIdentityPredicate, uriPredicate));
}
codePredicates.add(uriPredicate);
}
} else {
@ -186,8 +183,11 @@ public class UriPredicateBuilder extends BaseSearchParamPredicateBuilder {
}
ComboCondition orPredicate = ComboCondition.or(codePredicates.toArray(new Condition[0]));
Condition outerPredicate = combineWithHashIdentityPredicate(getResourceType(), theParamName, orPredicate);
return outerPredicate;
if (predicateIsHash) {
return orPredicate;
} else {
return combineWithHashIdentityPredicate(getResourceType(), theParamName, orPredicate);
}
}

View File

@ -66,6 +66,7 @@ import ca.uhn.fhir.jpa.model.entity.ResourceTable;
import ca.uhn.fhir.jpa.model.sched.HapiJob;
import ca.uhn.fhir.jpa.model.sched.ISchedulerService;
import ca.uhn.fhir.jpa.model.sched.ScheduledJobDefinition;
import ca.uhn.fhir.jpa.model.util.JpaConstants;
import ca.uhn.fhir.jpa.term.api.ITermCodeSystemStorageSvc;
import ca.uhn.fhir.jpa.term.api.ITermDeferredStorageSvc;
import ca.uhn.fhir.jpa.term.api.ITermLoaderSvc;
@ -81,14 +82,17 @@ import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
import ca.uhn.fhir.util.CoverageIgnore;
import ca.uhn.fhir.util.FhirVersionIndependentConcept;
import ca.uhn.fhir.util.HapiExtensions;
import ca.uhn.fhir.util.StopWatch;
import ca.uhn.fhir.util.UrlUtil;
import ca.uhn.fhir.util.ValidateUtil;
import ca.uhn.fhir.util.FhirVersionIndependentConcept;
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 com.google.common.collect.ArrayListMultimap;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.apache.commons.lang3.time.DateUtils;
@ -137,6 +141,8 @@ import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.interceptor.NoRollbackRuleAttribute;
import org.springframework.transaction.interceptor.RuleBasedTransactionAttribute;
import org.springframework.transaction.support.TransactionCallback;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import org.springframework.transaction.support.TransactionTemplate;
import javax.annotation.Nonnull;
@ -158,19 +164,22 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import static org.apache.commons.lang3.ObjectUtils.defaultIfNull;
import static org.apache.commons.lang3.StringUtils.defaultString;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isEmpty;
@ -248,66 +257,64 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc {
return cs != null;
}
private void addCodeIfNotAlreadyAdded(IValueSetConceptAccumulator theValueSetCodeAccumulator, Set<String> theAddedCodes, TermConcept theConcept, boolean theAdd, AtomicInteger theCodeCounter, String theValueSetIncludeVersion) {
private boolean addCodeIfNotAlreadyAdded(IValueSetConceptAccumulator theValueSetCodeAccumulator, Set<String> theAddedCodes, TermConcept theConcept, boolean theAdd, String theValueSetIncludeVersion) {
String codeSystem = theConcept.getCodeSystemVersion().getCodeSystem().getCodeSystemUri();
String code = theConcept.getCode();
String display = theConcept.getDisplay();
Collection<TermConceptDesignation> designations = theConcept.getDesignations();
if (StringUtils.isNotEmpty(theValueSetIncludeVersion)) {
addCodeIfNotAlreadyAdded(theValueSetCodeAccumulator, theAddedCodes, designations, theAdd, theCodeCounter, codeSystem + "|" + theValueSetIncludeVersion, code, display);
return addCodeIfNotAlreadyAdded(theValueSetCodeAccumulator, theAddedCodes, designations, theAdd, codeSystem + "|" + theValueSetIncludeVersion, code, display);
} else {
addCodeIfNotAlreadyAdded(theValueSetCodeAccumulator, theAddedCodes, designations, theAdd, theCodeCounter, codeSystem, code, display);
return addCodeIfNotAlreadyAdded(theValueSetCodeAccumulator, theAddedCodes, designations, theAdd, codeSystem, code, display);
}
}
private void addCodeIfNotAlreadyAdded(IValueSetConceptAccumulator theValueSetCodeAccumulator, Set<String> theAddedCodes, Collection<TermConceptDesignation> theDesignations, boolean theAdd, AtomicInteger theCodeCounter, String theCodeSystem, String theCodeSystemVersion, String theCode, String theDisplay) {
private void addCodeIfNotAlreadyAdded(IValueSetConceptAccumulator theValueSetCodeAccumulator, Set<String> theAddedCodes, boolean theAdd, String theCodeSystem, String theCodeSystemVersion, String theCode, String theDisplay) {
if (StringUtils.isNotEmpty(theCodeSystemVersion)) {
if (isNoneBlank(theCodeSystem, theCode)) {
if (theAdd && theAddedCodes.add(theCodeSystem + "|" + theCode)) {
theValueSetCodeAccumulator.includeConceptWithDesignations(theCodeSystem + "|" + theCodeSystemVersion, theCode, theDisplay, theDesignations);
theCodeCounter.incrementAndGet();
theValueSetCodeAccumulator.includeConceptWithDesignations(theCodeSystem + "|" + theCodeSystemVersion, theCode, theDisplay, null);
}
if (!theAdd && theAddedCodes.remove(theCodeSystem + "|" + theCode)) {
theValueSetCodeAccumulator.excludeConcept(theCodeSystem + "|" + theCodeSystemVersion, theCode);
theCodeCounter.decrementAndGet();
}
}
} else {
if (theAdd && theAddedCodes.add(theCodeSystem + "|" + theCode)) {
theValueSetCodeAccumulator.includeConceptWithDesignations(theCodeSystem, theCode, theDisplay, theDesignations);
theCodeCounter.incrementAndGet();
theValueSetCodeAccumulator.includeConceptWithDesignations(theCodeSystem, theCode, theDisplay, null);
}
if (!theAdd && theAddedCodes.remove(theCodeSystem + "|" + theCode)) {
theValueSetCodeAccumulator.excludeConcept(theCodeSystem, theCode);
theCodeCounter.decrementAndGet();
}
}
}
private void addCodeIfNotAlreadyAdded(IValueSetConceptAccumulator theValueSetCodeAccumulator, Set<String> theAddedCodes, Collection<TermConceptDesignation> theDesignations, boolean theAdd, AtomicInteger theCodeCounter, String theCodeSystem, String theCode, String theDisplay) {
private boolean addCodeIfNotAlreadyAdded(IValueSetConceptAccumulator theValueSetCodeAccumulator, Set<String> theAddedCodes, Collection<TermConceptDesignation> theDesignations, boolean theAdd, String theCodeSystem, String theCode, String theDisplay) {
if (isNoneBlank(theCodeSystem, theCode)) {
if (theAdd && theAddedCodes.add(theCodeSystem + "|" + theCode)) {
theValueSetCodeAccumulator.includeConceptWithDesignations(theCodeSystem, theCode, theDisplay, theDesignations);
theCodeCounter.incrementAndGet();
return true;
}
if (!theAdd && theAddedCodes.remove(theCodeSystem + "|" + theCode)) {
theValueSetCodeAccumulator.excludeConcept(theCodeSystem, theCode);
theCodeCounter.decrementAndGet();
return true;
}
}
return false;
}
private void addConceptsToList(IValueSetConceptAccumulator theValueSetCodeAccumulator, Set<String> theAddedCodes, String theSystem, List<CodeSystem.ConceptDefinitionComponent> theConcept, boolean theAdd, FhirVersionIndependentConcept theWantConceptOrNull) {
private void addConceptsToList(IValueSetConceptAccumulator theValueSetCodeAccumulator, Set<String> theAddedCodes, String theSystem, List<CodeSystem.ConceptDefinitionComponent> theConcept, boolean theAdd, @Nonnull ExpansionFilter theExpansionFilter) {
for (CodeSystem.ConceptDefinitionComponent next : theConcept) {
if (isNoneBlank(theSystem, next.getCode())) {
if (theWantConceptOrNull == null || theWantConceptOrNull.getCode().equals(next.getCode())) {
if (!theExpansionFilter.hasCode() || theExpansionFilter.getCode().equals(next.getCode())) {
addOrRemoveCode(theValueSetCodeAccumulator, theAddedCodes, theAdd, theSystem, next.getCode(), next.getDisplay());
}
}
addConceptsToList(theValueSetCodeAccumulator, theAddedCodes, theSystem, next.getConcept(), theAdd, theWantConceptOrNull);
addConceptsToList(theValueSetCodeAccumulator, theAddedCodes, theSystem, next.getConcept(), theAdd, theExpansionFilter);
}
}
@ -387,28 +394,14 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc {
deleteValueSetForResource(theResourceTable);
}
private ValueSet expandValueSetInMemory(ValueSetExpansionOptions theExpansionOptions, ValueSet theValueSetToExpand, FhirVersionIndependentConcept theWantConceptOrNull) {
int maxCapacity = myDaoConfig.getMaximumExpansionSize();
ValueSetExpansionComponentWithConceptAccumulator expansionComponent = new ValueSetExpansionComponentWithConceptAccumulator(myContext, maxCapacity);
expansionComponent.setIdentifier(UUID.randomUUID().toString());
expansionComponent.setTimestamp(new Date());
AtomicInteger codeCounter = new AtomicInteger(0);
expandValueSet(theExpansionOptions, theValueSetToExpand, expansionComponent, codeCounter, theWantConceptOrNull);
expansionComponent.setTotal(codeCounter.get());
ValueSet valueSet = new ValueSet();
valueSet.setStatus(Enumerations.PublicationStatus.ACTIVE);
valueSet.setCompose(theValueSetToExpand.getCompose());
valueSet.setExpansion(expansionComponent);
return valueSet;
}
@Override
public List<FhirVersionIndependentConcept> expandValueSet(ValueSetExpansionOptions theExpansionOptions, String theValueSet) {
ExpansionFilter expansionFilter = ExpansionFilter.NO_FILTER;
return expandValueSet(theExpansionOptions, theValueSet, expansionFilter);
}
private List<FhirVersionIndependentConcept> expandValueSet(ValueSetExpansionOptions theExpansionOptions, String theValueSet, ExpansionFilter theExpansionFilter) {
// TODO: DM 2019-09-10 - This is problematic because an incorrect URL that matches ValueSet.id will not be found in the terminology tables but will yield a ValueSet here. Depending on the ValueSet, the expansion may time-out.
ValueSet valueSet = fetchCanonicalValueSetFromCompleteContext(theValueSet);
@ -416,7 +409,7 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc {
throwInvalidValueSet(theValueSet);
}
return expandValueSetAndReturnVersionIndependentConcepts(theExpansionOptions, valueSet, null);
return expandValueSetAndReturnVersionIndependentConcepts(theExpansionOptions, valueSet, theExpansionFilter);
}
@Override
@ -424,6 +417,47 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc {
public ValueSet expandValueSet(ValueSetExpansionOptions theExpansionOptions, ValueSet theValueSetToExpand) {
ValidateUtil.isNotNullOrThrowUnprocessableEntity(theValueSetToExpand, "ValueSet to expand can not be null");
ValueSetExpansionOptions expansionOptions = provideExpansionOptions(theExpansionOptions);
int offset = expansionOptions.getOffset();
int count = expansionOptions.getCount();
ValueSetExpansionComponentWithConceptAccumulator accumulator = new ValueSetExpansionComponentWithConceptAccumulator(myContext, count);
accumulator.setHardExpansionMaximumSize(myDaoConfig.getMaximumExpansionSize());
accumulator.setSkipCountRemaining(offset);
accumulator.setIdentifier(UUID.randomUUID().toString());
accumulator.setTimestamp(new Date());
accumulator.setOffset(offset);
if (theExpansionOptions != null) {
accumulator.addParameter().setName("offset").setValue(new IntegerType(offset));
}
if (theExpansionOptions != null) {
accumulator.addParameter().setName("count").setValue(new IntegerType(count));
}
ExpansionFilter filter = ExpansionFilter.NO_FILTER;
expandValueSetIntoAccumulator(theValueSetToExpand, theExpansionOptions, accumulator, filter, true);
if (accumulator.getTotalConcepts() != null) {
accumulator.setTotal(accumulator.getTotalConcepts());
}
ValueSet valueSet = new ValueSet();
valueSet.setStatus(Enumerations.PublicationStatus.ACTIVE);
valueSet.setCompose(theValueSetToExpand.getCompose());
valueSet.setExpansion(accumulator);
for (String next : accumulator.getMessages()) {
valueSet.getMeta().addExtension()
.setUrl(HapiExtensions.EXT_VALUESET_EXPANSION_MESSAGE)
.setValue(new StringType(next));
}
return valueSet;
}
private void expandValueSetIntoAccumulator(ValueSet theValueSetToExpand, ValueSetExpansionOptions theExpansionOptions, IValueSetConceptAccumulator theAccumulator, ExpansionFilter theFilter, boolean theAdd) {
Optional<TermValueSet> optionalTermValueSet;
if (theValueSetToExpand.hasUrl()) {
if (theValueSetToExpand.hasVersion()) {
@ -440,85 +474,87 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc {
optionalTermValueSet = Optional.empty();
}
/*
* ValueSet doesn't exist in pre-expansion database, so perform in-memory expansion
*/
if (!optionalTermValueSet.isPresent()) {
ourLog.debug("ValueSet is not present in terminology tables. Will perform in-memory expansion without parameters. {}", getValueSetInfo(theValueSetToExpand));
return expandValueSetInMemory(theExpansionOptions, theValueSetToExpand, null); // In-memory expansion.
}
TermValueSet termValueSet = optionalTermValueSet.get();
if (termValueSet.getExpansionStatus() != TermValueSetPreExpansionStatusEnum.EXPANDED) {
ourLog.warn("{} is present in terminology tables but not ready for persistence-backed invocation of operation $expand. Will perform in-memory expansion without parameters. Current status: {} | {}",
getValueSetInfo(theValueSetToExpand), termValueSet.getExpansionStatus().name(), termValueSet.getExpansionStatus().getDescription());
return expandValueSetInMemory(theExpansionOptions, theValueSetToExpand, null); // In-memory expansion.
}
ValueSet.ValueSetExpansionComponent expansionComponent = new ValueSet.ValueSetExpansionComponent();
expansionComponent.setIdentifier(UUID.randomUUID().toString());
expansionComponent.setTimestamp(new Date());
ValueSetExpansionOptions expansionOptions = provideExpansionOptions(theExpansionOptions);
int offset = expansionOptions.getOffset();
int count = expansionOptions.getCount();
populateExpansionComponent(expansionComponent, termValueSet, offset, count);
ValueSet valueSet = new ValueSet();
valueSet.setStatus(Enumerations.PublicationStatus.ACTIVE);
valueSet.setCompose(theValueSetToExpand.getCompose());
valueSet.setExpansion(expansionComponent);
return valueSet;
}
private void populateExpansionComponent(ValueSet.ValueSetExpansionComponent theExpansionComponent, TermValueSet theTermValueSet, int theOffset, int theCount) {
int total = theTermValueSet.getTotalConcepts().intValue();
theExpansionComponent.setTotal(total);
theExpansionComponent.setOffset(theOffset);
theExpansionComponent.addParameter().setName("offset").setValue(new IntegerType(theOffset));
theExpansionComponent.addParameter().setName("count").setValue(new IntegerType(theCount));
if (theCount == 0) {
expandValueSet(theExpansionOptions, theValueSetToExpand, theAccumulator, theFilter);
return;
}
expandConcepts(theExpansionComponent, theTermValueSet, theOffset, theCount);
/*
* ValueSet exists in pre-expansion database, but pre-expansion is not yet complete so perform in-memory expansion
*/
TermValueSet termValueSet = optionalTermValueSet.get();
if (termValueSet.getExpansionStatus() != TermValueSetPreExpansionStatusEnum.EXPANDED) {
String msg = myContext.getLocalizer().getMessage(BaseTermReadSvcImpl.class, "valueSetNotYetExpanded", getValueSetInfo(theValueSetToExpand), termValueSet.getExpansionStatus().name(), termValueSet.getExpansionStatus().getDescription());
theAccumulator.addMessage(msg);
expandValueSet(theExpansionOptions, theValueSetToExpand, theAccumulator, theFilter);
return;
}
/*
* ValueSet is pre-expanded in database so let's use that
*/
expandConcepts(theAccumulator, termValueSet, theFilter, theAdd);
}
private void expandConcepts(ValueSet.ValueSetExpansionComponent theExpansionComponent, TermValueSet theTermValueSet, int theOffset, int theCount) {
private void expandConcepts(IValueSetConceptAccumulator theAccumulator, TermValueSet theTermValueSet, ExpansionFilter theFilter, boolean theAdd) {
Integer offset = theAccumulator.getSkipCountRemaining();
offset = ObjectUtils.defaultIfNull(offset, 0);
offset = Math.min(offset, theTermValueSet.getTotalConcepts().intValue());
Integer count = theAccumulator.getCapacityRemaining();
count = defaultIfNull(count, myDaoConfig.getMaximumExpansionSize());
int conceptsExpanded = 0;
int designationsExpanded = 0;
int toIndex = theOffset + theCount;
Collection<TermValueSetConceptView> conceptViews = myTermValueSetConceptViewDao.findByTermValueSetId(theOffset, toIndex, theTermValueSet.getId());
int toIndex = offset + count;
Collection<TermValueSetConceptView> conceptViews;
boolean wasFilteredResult = false;
if (!theFilter.getFilters().isEmpty() && JpaConstants.VALUESET_FILTER_DISPLAY.equals(theFilter.getFilters().get(0).getProperty()) && theFilter.getFilters().get(0).getOp() == ValueSet.FilterOperator.EQUAL) {
String displayValue = theFilter.getFilters().get(0).getValue().replace("%", "[%]") + "%";
conceptViews = myTermValueSetConceptViewDao.findByTermValueSetId(theTermValueSet.getId(), displayValue);
wasFilteredResult = true;
} else {
conceptViews = myTermValueSetConceptViewDao.findByTermValueSetId(offset, toIndex, theTermValueSet.getId());
theAccumulator.consumeSkipCount(offset);
if (theAdd) {
theAccumulator.incrementOrDecrementTotalConcepts(true, theTermValueSet.getTotalConcepts().intValue());
}
}
if (conceptViews.isEmpty()) {
logConceptsExpanded("No concepts to expand. ", theTermValueSet, conceptsExpanded);
return;
}
Map<Long, ValueSet.ValueSetExpansionContainsComponent> pidToConcept = new HashMap<>();
Map<Long, FhirVersionIndependentConcept> pidToConcept = new LinkedHashMap<>();
ArrayListMultimap<Long, TermConceptDesignation> pidToDesignations = ArrayListMultimap.create();
for (TermValueSetConceptView conceptView : conceptViews) {
Long conceptPid = conceptView.getConceptPid();
ValueSet.ValueSetExpansionContainsComponent containsComponent;
if (!pidToConcept.containsKey(conceptPid)) {
containsComponent = theExpansionComponent.addContains();
containsComponent.setSystem(conceptView.getConceptSystemUrl());
containsComponent.setCode(conceptView.getConceptCode());
containsComponent.setDisplay(conceptView.getConceptDisplay());
pidToConcept.put(conceptPid, containsComponent);
} else {
containsComponent = pidToConcept.get(conceptPid);
String system = conceptView.getConceptSystemUrl();
String code = conceptView.getConceptCode();
String display = conceptView.getConceptDisplay();
FhirVersionIndependentConcept concept = new FhirVersionIndependentConcept(system, code, display);
pidToConcept.put(conceptPid, concept);
}
// TODO: DM 2019-08-17 - Implement includeDesignations parameter for $expand operation to designations optional.
if (conceptView.getDesignationPid() != null) {
ValueSet.ConceptReferenceDesignationComponent designationComponent = containsComponent.addDesignation();
designationComponent.setLanguage(conceptView.getDesignationLang());
designationComponent.setUse(new Coding(
conceptView.getDesignationUseSystem(),
conceptView.getDesignationUseCode(),
conceptView.getDesignationUseDisplay()));
designationComponent.setValue(conceptView.getDesignationVal());
TermConceptDesignation designation = new TermConceptDesignation();
designation.setUseSystem(conceptView.getDesignationUseSystem());
designation.setUseCode(conceptView.getDesignationUseCode());
designation.setUseDisplay(conceptView.getDesignationUseDisplay());
designation.setValue(conceptView.getDesignationVal());
designation.setLanguage(conceptView.getDesignationLang());
pidToDesignations.put(conceptPid, designation);
if (++designationsExpanded % 250 == 0) {
logDesignationsExpanded("Expansion of designations in progress. ", theTermValueSet, designationsExpanded);
@ -530,6 +566,33 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc {
}
}
for (Long nextPid : pidToConcept.keySet()) {
FhirVersionIndependentConcept concept = pidToConcept.get(nextPid);
List<TermConceptDesignation> designations = pidToDesignations.get(nextPid);
String system = concept.getSystem();
String code = concept.getCode();
String display = concept.getDisplay();
if (theAdd) {
if (theAccumulator.getCapacityRemaining() != null) {
if (theAccumulator.getCapacityRemaining() == 0) {
break;
}
}
theAccumulator.includeConceptWithDesignations(system, code, display, designations);
} else {
boolean removed = theAccumulator.excludeConcept(system, code);
if (removed) {
theAccumulator.incrementOrDecrementTotalConcepts(false, 1);
}
}
}
if (wasFilteredResult && theAdd) {
theAccumulator.incrementOrDecrementTotalConcepts(true, pidToConcept.size());
}
logDesignationsExpanded("Finished expanding designations. ", theTermValueSet, designationsExpanded);
logConceptsExpanded("Finished expanding concepts. ", theTermValueSet, conceptsExpanded);
}
@ -549,25 +612,34 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc {
@Override
@Transactional(propagation = Propagation.REQUIRED)
public void expandValueSet(ValueSetExpansionOptions theExpansionOptions, ValueSet theValueSetToExpand, IValueSetConceptAccumulator theValueSetCodeAccumulator) {
expandValueSet(theExpansionOptions, theValueSetToExpand, theValueSetCodeAccumulator, new AtomicInteger(0), null);
expandValueSet(theExpansionOptions, theValueSetToExpand, theValueSetCodeAccumulator, ExpansionFilter.NO_FILTER);
}
@SuppressWarnings("ConstantConditions")
private void expandValueSet(ValueSetExpansionOptions theExpansionOptions, ValueSet theValueSetToExpand, IValueSetConceptAccumulator theValueSetCodeAccumulator, AtomicInteger theCodeCounter, FhirVersionIndependentConcept theWantConceptOrNull) {
private void expandValueSet(ValueSetExpansionOptions theExpansionOptions, ValueSet theValueSetToExpand, IValueSetConceptAccumulator theValueSetCodeAccumulator, @Nonnull ExpansionFilter theExpansionFilter) {
Set<String> addedCodes = new HashSet<>();
StopWatch sw = new StopWatch();
String valueSetInfo = getValueSetInfo(theValueSetToExpand);
ourLog.debug("Working with {}", valueSetInfo);
// Offset can't be combined with excludes
Integer skipCountRemaining = theValueSetCodeAccumulator.getSkipCountRemaining();
if (skipCountRemaining != null && skipCountRemaining > 0) {
if (theValueSetToExpand.getCompose().getExclude().size() > 0) {
String msg = myContext.getLocalizer().getMessage(BaseTermReadSvcImpl.class, "valueSetNotYetExpanded_OffsetNotAllowed", valueSetInfo);
throw new InvalidRequestException(msg);
}
}
// Handle includes
ourLog.debug("Handling includes");
for (ValueSet.ConceptSetComponent include : theValueSetToExpand.getCompose().getInclude()) {
for (int i = 0; ; i++) {
int queryIndex = i;
Boolean shouldContinue = myTxTemplate.execute(t -> {
Boolean shouldContinue = executeInNewTransactionIfNeeded(() -> {
boolean add = true;
return expandValueSetHandleIncludeOrExclude(theExpansionOptions, theValueSetCodeAccumulator, addedCodes, include, add, theCodeCounter, queryIndex, theWantConceptOrNull);
return expandValueSetHandleIncludeOrExclude(theExpansionOptions, theValueSetCodeAccumulator, addedCodes, include, add, queryIndex, theExpansionFilter);
});
if (!shouldContinue) {
break;
@ -575,20 +647,15 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc {
}
}
// If the accumulator filled up, abort
if (theValueSetCodeAccumulator.getCapacityRemaining() != null && theValueSetCodeAccumulator.getCapacityRemaining() <= 0) {
String msg = myContext.getLocalizer().getMessage(BaseTermReadSvcImpl.class, "expansionTooLarge", myDaoConfig.getMaximumExpansionSize());
throw new ExpansionTooCostlyException(msg);
}
// Handle excludes
ourLog.debug("Handling excludes");
for (ValueSet.ConceptSetComponent exclude : theValueSetToExpand.getCompose().getExclude()) {
for (int i = 0; ; i++) {
int queryIndex = i;
Boolean shouldContinue = myTxTemplate.execute(t -> {
Boolean shouldContinue = executeInNewTransactionIfNeeded(() -> {
boolean add = false;
return expandValueSetHandleIncludeOrExclude(theExpansionOptions, theValueSetCodeAccumulator, addedCodes, exclude, add, theCodeCounter, queryIndex, null);
ExpansionFilter expansionFilter = ExpansionFilter.NO_FILTER;
return expandValueSetHandleIncludeOrExclude(theExpansionOptions, theValueSetCodeAccumulator, addedCodes, exclude, add, queryIndex, expansionFilter);
});
if (!shouldContinue) {
break;
@ -603,47 +670,49 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc {
ourLog.debug("Done working with {} in {}ms", valueSetInfo, sw.getMillis());
}
/**
* Execute in a new transaction only if we aren't already in one. We do this because in some cases
* when performing a VS expansion we throw an {@link ExpansionTooCostlyException} and we don't want
* this to cause the TX to be marked a rollback prematurely.
*/
private <T> T executeInNewTransactionIfNeeded(Supplier<T> theAction) {
if (TransactionSynchronizationManager.isSynchronizationActive()) {
return theAction.get();
}
return myTxTemplate.execute(t->theAction.get());
}
private String getValueSetInfo(ValueSet theValueSet) {
StringBuilder sb = new StringBuilder();
boolean isIdentified = false;
sb
.append("ValueSet:");
if (theValueSet.hasId()) {
isIdentified = true;
sb
.append(" ValueSet.id[")
.append(theValueSet.getId())
.append("]");
}
if (theValueSet.hasUrl()) {
isIdentified = true;
sb
.append(" ValueSet.url[")
.append("ValueSet.url[")
.append(theValueSet.getUrl())
.append("]");
}
if (theValueSet.hasIdentifier()) {
} else if (theValueSet.hasId()) {
isIdentified = true;
sb
.append(" ValueSet.identifier[")
.append(theValueSet.getIdentifierFirstRep().getSystem())
.append("|")
.append(theValueSet.getIdentifierFirstRep().getValue())
.append("ValueSet.id[")
.append(theValueSet.getId())
.append("]");
}
if (!isIdentified) {
sb.append(" None of ValueSet.id, ValueSet.url, and ValueSet.identifier are provided.");
sb.append("Unidentified ValueSet");
}
return sb.toString();
}
protected List<FhirVersionIndependentConcept> expandValueSetAndReturnVersionIndependentConcepts(ValueSetExpansionOptions theExpansionOptions, ValueSet theValueSetToExpandR4, FhirVersionIndependentConcept theWantConceptOrNull) {
org.hl7.fhir.r4.model.ValueSet.ValueSetExpansionComponent expandedR4 = expandValueSetInMemory(theExpansionOptions, theValueSetToExpandR4, theWantConceptOrNull).getExpansion();
protected List<FhirVersionIndependentConcept> expandValueSetAndReturnVersionIndependentConcepts(ValueSetExpansionOptions theExpansionOptions, ValueSet theValueSetToExpandR4, @Nonnull ExpansionFilter theExpansionFilter) {
int maxCapacity = myDaoConfig.getMaximumExpansionSize();
ValueSetExpansionComponentWithConceptAccumulator accumulator = new ValueSetExpansionComponentWithConceptAccumulator(myContext, maxCapacity);
expandValueSet(theExpansionOptions, theValueSetToExpandR4, accumulator, theExpansionFilter);
ArrayList<FhirVersionIndependentConcept> retVal = new ArrayList<>();
for (org.hl7.fhir.r4.model.ValueSet.ValueSetExpansionContainsComponent nextContains : expandedR4.getContains()) {
for (org.hl7.fhir.r4.model.ValueSet.ValueSetExpansionContainsComponent nextContains : accumulator.getContains()) {
retVal.add(new FhirVersionIndependentConcept(nextContains.getSystem(), nextContains.getCode(), nextContains.getDisplay(), nextContains.getVersion()));
}
return retVal;
@ -652,7 +721,7 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc {
/**
* @return Returns true if there are potentially more results to process.
*/
private Boolean expandValueSetHandleIncludeOrExclude(@Nullable ValueSetExpansionOptions theExpansionOptions, IValueSetConceptAccumulator theValueSetCodeAccumulator, Set<String> theAddedCodes, ValueSet.ConceptSetComponent theIncludeOrExclude, boolean theAdd, AtomicInteger theCodeCounter, int theQueryIndex, FhirVersionIndependentConcept theWantConceptOrNull) {
private Boolean expandValueSetHandleIncludeOrExclude(@Nullable ValueSetExpansionOptions theExpansionOptions, IValueSetConceptAccumulator theValueSetCodeAccumulator, Set<String> theAddedCodes, ValueSet.ConceptSetComponent theIncludeOrExclude, boolean theAdd, int theQueryIndex, @Nonnull ExpansionFilter theExpansionFilter) {
String system = theIncludeOrExclude.getSystem();
boolean hasSystem = isNotBlank(system);
@ -660,7 +729,7 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc {
if (hasSystem) {
if (theWantConceptOrNull != null && theWantConceptOrNull.getSystem() != null && !system.equals(theWantConceptOrNull.getSystem())) {
if (theExpansionFilter.hasCode() && theExpansionFilter.getSystem() != null && !system.equals(theExpansionFilter.getSystem())) {
return false;
}
@ -669,13 +738,13 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc {
TermCodeSystem cs = myCodeSystemDao.findByCodeSystemUri(system);
if (cs != null) {
return expandValueSetHandleIncludeOrExcludeUsingDatabase(theValueSetCodeAccumulator, theAddedCodes, theIncludeOrExclude, theAdd, theCodeCounter, theQueryIndex, theWantConceptOrNull, system, cs);
return expandValueSetHandleIncludeOrExcludeUsingDatabase(theValueSetCodeAccumulator, theAddedCodes, theIncludeOrExclude, theAdd, theQueryIndex, theExpansionFilter, system, cs);
} else {
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()))) {
if (theIncludeOrExclude.getConcept().size() > 0 && theExpansionFilter.hasCode()) {
if (defaultString(theIncludeOrExclude.getSystem()).equals(theExpansionFilter.getSystem())) {
if (theIncludeOrExclude.getConcept().stream().noneMatch(t -> t.getCode().equals(theExpansionFilter.getCode()))) {
return false;
}
}
@ -691,9 +760,9 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc {
// 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<FhirVersionIndependentConcept> includedConcepts = null;
if (theWantConceptOrNull != null) {
if (theExpansionFilter.hasCode()) {
includedConcepts = new ArrayList<>();
includedConcepts.add(theWantConceptOrNull);
includedConcepts.add(theExpansionFilter.toFhirVersionIndependentConcept());
} else if (!theIncludeOrExclude.getConcept().isEmpty()) {
includedConcepts = theIncludeOrExclude
.getConcept()
@ -737,7 +806,7 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc {
if (!theIncludeOrExclude.getConcept().isEmpty()) {
for (ValueSet.ConceptReferenceComponent next : theIncludeOrExclude.getConcept()) {
String nextCode = next.getCode();
if (theWantConceptOrNull == null || theWantConceptOrNull.getCode().equals(nextCode)) {
if (!theExpansionFilter.hasCode() || theExpansionFilter.getCode().equals(nextCode)) {
if (isNoneBlank(system, nextCode) && !theAddedCodes.contains(system + "|" + nextCode)) {
CodeSystem.ConceptDefinitionComponent code = findCode(codeSystemFromContext.getConcept(), nextCode);
@ -751,7 +820,7 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc {
}
} else {
List<CodeSystem.ConceptDefinitionComponent> concept = codeSystemFromContext.getConcept();
addConceptsToList(theValueSetCodeAccumulator, theAddedCodes, system, concept, theAdd, theWantConceptOrNull);
addConceptsToList(theValueSetCodeAccumulator, theAddedCodes, system, concept, theAdd, theExpansionFilter);
}
return false;
@ -760,44 +829,20 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc {
} else if (hasValueSet) {
for (CanonicalType nextValueSet : theIncludeOrExclude.getValueSet()) {
ourLog.debug("Starting {} expansion around ValueSet: {}", (theAdd ? "inclusion" : "exclusion"), nextValueSet.getValueAsString());
String valueSetUrl = nextValueSet.getValueAsString();
ourLog.debug("Starting {} expansion around ValueSet: {}", (theAdd ? "inclusion" : "exclusion"), valueSetUrl);
List<FhirVersionIndependentConcept> expanded = expandValueSet(theExpansionOptions, nextValueSet.getValueAsString());
Map<String, TermCodeSystem> uriToCodeSystem = new HashMap<>();
ExpansionFilter subExpansionFilter = new ExpansionFilter(theExpansionFilter, theIncludeOrExclude.getFilter(), theValueSetCodeAccumulator.getCapacityRemaining());
for (FhirVersionIndependentConcept nextConcept : expanded) {
if (theAdd) {
// TODO: DM 2019-09-10 - This is problematic because an incorrect URL that matches ValueSet.id will not be found in the terminology tables but will yield a ValueSet here. Depending on the ValueSet, the expansion may time-out.
if (!uriToCodeSystem.containsKey(nextConcept.getSystem())) {
TermCodeSystem codeSystem = myCodeSystemDao.findByCodeSystemUri(nextConcept.getSystem());
uriToCodeSystem.put(nextConcept.getSystem(), codeSystem);
}
TermCodeSystem codeSystem = uriToCodeSystem.get(nextConcept.getSystem());
if (codeSystem != null) {
TermCodeSystemVersion termCodeSystemVersion;
if (nextConcept.getSystemVersion() != null) {
termCodeSystemVersion = myCodeSystemVersionDao.findByCodeSystemPidAndVersion(codeSystem.getPid(), nextConcept.getSystemVersion());
} else {
termCodeSystemVersion = codeSystem.getCurrentVersion();
}
myConceptDao
.findByCodeSystemAndCode(termCodeSystemVersion, nextConcept.getCode())
.ifPresent(concept ->
addCodeIfNotAlreadyAdded(theValueSetCodeAccumulator, theAddedCodes, concept, theAdd, theCodeCounter, nextConcept.getSystemVersion())
);
} else {
// This will happen if we're expanding against a built-in (part of FHIR) ValueSet that
// isn't actually in the database anywhere
Collection<TermConceptDesignation> emptyCollection = Collections.emptyList();
addCodeIfNotAlreadyAdded(theValueSetCodeAccumulator, theAddedCodes, emptyCollection, theAdd, theCodeCounter, nextConcept.getSystem(), nextConcept.getSystemVersion(), nextConcept.getCode(), nextConcept.getDisplay());
}
}
if (isNoneBlank(nextConcept.getSystem(), nextConcept.getCode()) && !theAdd && theAddedCodes.remove(nextConcept.getSystem() + "|" + nextConcept.getCode())) {
theValueSetCodeAccumulator.excludeConcept(nextConcept.getSystem(), nextConcept.getCode());
}
ValueSet valueSet = fetchCanonicalValueSetFromCompleteContext(valueSetUrl);
if (valueSet == null) {
throw new ResourceNotFoundException("Unknown ValueSet: " + UrlUtil.escapeUrlParam(valueSetUrl));
}
expandValueSetIntoAccumulator(valueSet, theExpansionOptions, theValueSetCodeAccumulator, subExpansionFilter, theAdd);
}
return false;
@ -810,7 +855,7 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc {
}
@Nonnull
private Boolean expandValueSetHandleIncludeOrExcludeUsingDatabase(IValueSetConceptAccumulator theValueSetCodeAccumulator, Set<String> theAddedCodes, ValueSet.ConceptSetComponent theIncludeOrExclude, boolean theAdd, AtomicInteger theCodeCounter, int theQueryIndex, FhirVersionIndependentConcept theWantConceptOrNull, String theSystem, TermCodeSystem theCs) {
private Boolean expandValueSetHandleIncludeOrExcludeUsingDatabase(IValueSetConceptAccumulator theValueSetCodeAccumulator, Set<String> theAddedCodes, ValueSet.ConceptSetComponent theIncludeOrExclude, boolean theAdd, int theQueryIndex, @Nonnull ExpansionFilter theExpansionFilter, String theSystem, TermCodeSystem theCs) {
String includeOrExcludeVersion = theIncludeOrExclude.getVersion();
TermCodeSystemVersion csv;
if (isEmpty(includeOrExcludeVersion)) {
@ -825,7 +870,7 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc {
* since we're going to do it without the database.
*/
if (myFulltextSearchSvc == null) {
expandWithoutHibernateSearch(theValueSetCodeAccumulator, csv, theAddedCodes, theIncludeOrExclude, theSystem, theAdd, theCodeCounter);
expandWithoutHibernateSearch(theValueSetCodeAccumulator, csv, theAddedCodes, theIncludeOrExclude, theSystem, theAdd);
return false;
}
@ -837,8 +882,8 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc {
bool.must(qb.keyword().onField("myCodeSystemVersionPid").matching(csv.getPid()).createQuery());
if (theWantConceptOrNull != null) {
bool.must(qb.keyword().onField("myCode").matching(theWantConceptOrNull.getCode()).createQuery());
if (theExpansionFilter.hasCode()) {
bool.must(qb.keyword().onField("myCode").matching(theExpansionFilter.getCode()).createQuery());
}
/*
@ -850,7 +895,12 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc {
} else {
codeSystemUrlAndVersion = theSystem;
}
handleFilters(bool, codeSystemUrlAndVersion, qb, theIncludeOrExclude);
for (ValueSet.ConceptSetFilterComponent nextFilter : theIncludeOrExclude.getFilter()) {
handleFilter(codeSystemUrlAndVersion, qb, bool, nextFilter);
}
for (ValueSet.ConceptSetFilterComponent nextFilter : theExpansionFilter.getFilters()) {
handleFilter(codeSystemUrlAndVersion, qb, bool, nextFilter);
}
Query luceneQuery = bool.createQuery();
@ -917,21 +967,22 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc {
StopWatch swForBatch = new StopWatch();
AtomicInteger countForBatch = new AtomicInteger(0);
List resultList = jpaQuery.getResultList();
List<?> resultList = jpaQuery.getResultList();
int resultsInBatch = resultList.size();
int firstResult = jpaQuery.getFirstResult();
int delta = 0;
for (Object next : resultList) {
count.incrementAndGet();
countForBatch.incrementAndGet();
TermConcept concept = (TermConcept) next;
try {
addCodeIfNotAlreadyAdded(theValueSetCodeAccumulator, theAddedCodes, concept, theAdd, theCodeCounter, includeOrExcludeVersion);
} catch (ExpansionTooCostlyException e) {
return false;
boolean added = addCodeIfNotAlreadyAdded(theValueSetCodeAccumulator, theAddedCodes, concept, theAdd, includeOrExcludeVersion);
if (added) {
delta++;
}
}
ourLog.debug("Batch expansion for {} with starting index of {} produced {} results in {}ms", (theAdd ? "inclusion" : "exclusion"), firstResult, countForBatch, swForBatch.getMillis());
theValueSetCodeAccumulator.incrementOrDecrementTotalConcepts(theAdd, delta);
if (resultsInBatch < maxResultsPerBatch) {
ourLog.debug("Expansion for {} produced {} results in {}ms", (theAdd ? "inclusion" : "exclusion"), count, sw.getMillis());
@ -959,14 +1010,6 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc {
}
}
private void handleFilters(BooleanJunction<?> theBool, String theCodeSystemIdentifier, QueryBuilder theQb, ValueSet.ConceptSetComponent theIncludeOrExclude) {
if (theIncludeOrExclude.getFilter().size() > 0) {
for (ValueSet.ConceptSetFilterComponent nextFilter : theIncludeOrExclude.getFilter()) {
handleFilter(theCodeSystemIdentifier, theQb, theBool, nextFilter);
}
}
}
private void handleFilter(String theCodeSystemIdentifier, QueryBuilder theQb, BooleanJunction<?> theBool, ValueSet.ConceptSetFilterComponent theFilter) {
if (isBlank(theFilter.getValue()) && theFilter.getOp() == null && isBlank(theFilter.getProperty())) {
return;
@ -1264,7 +1307,7 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc {
}
}
private void expandWithoutHibernateSearch(IValueSetConceptAccumulator theValueSetCodeAccumulator, TermCodeSystemVersion theVersion, Set<String> theAddedCodes, ValueSet.ConceptSetComponent theInclude, String theSystem, boolean theAdd, AtomicInteger theCodeCounter) {
private void expandWithoutHibernateSearch(IValueSetConceptAccumulator theValueSetCodeAccumulator, TermCodeSystemVersion theVersion, Set<String> theAddedCodes, ValueSet.ConceptSetComponent theInclude, String theSystem, boolean theAdd) {
ourLog.trace("Hibernate search is not enabled");
if (theValueSetCodeAccumulator instanceof ValueSetExpansionComponentWithConceptAccumulator) {
@ -1277,7 +1320,7 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc {
if (theInclude.getConcept().isEmpty()) {
for (TermConcept next : theVersion.getConcepts()) {
addCodeIfNotAlreadyAdded(theValueSetCodeAccumulator, theAddedCodes, null, theAdd, theCodeCounter, theSystem, theInclude.getVersion(), next.getCode(), next.getDisplay());
addCodeIfNotAlreadyAdded(theValueSetCodeAccumulator, theAddedCodes, theAdd, theSystem, theInclude.getVersion(), next.getCode(), next.getDisplay());
}
}
@ -1285,7 +1328,7 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc {
if (!theSystem.equals(theInclude.getSystem()) && isNotBlank(theSystem)) {
continue;
}
addCodeIfNotAlreadyAdded(theValueSetCodeAccumulator, theAddedCodes, null, theAdd, theCodeCounter, theSystem, theInclude.getVersion(), next.getCode(), next.getDisplay());
addCodeIfNotAlreadyAdded(theValueSetCodeAccumulator, theAddedCodes, theAdd, theSystem, theInclude.getVersion(), next.getCode(), next.getDisplay());
}
@ -1447,14 +1490,13 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc {
@Nullable
private TermCodeSystemVersion getCurrentCodeSystemVersion(String theCodeSystemIdentifier) {
String myVersion = getVersionFromIdentifier(theCodeSystemIdentifier);
String key = theCodeSystemIdentifier;
TermCodeSystemVersion retVal = myCodeSystemCurrentVersionCache.get(key.toString(), t -> myTxTemplate.execute(tx -> {
String version = getVersionFromIdentifier(theCodeSystemIdentifier);
TermCodeSystemVersion retVal = myCodeSystemCurrentVersionCache.get(theCodeSystemIdentifier, t -> myTxTemplate.execute(tx -> {
TermCodeSystemVersion csv = null;
TermCodeSystem cs = myCodeSystemDao.findByCodeSystemUri(getUrlFromIdentifier(theCodeSystemIdentifier));
if (cs != null) {
if (myVersion != null) {
csv = myCodeSystemVersionDao.findByCodeSystemPidAndVersion(cs.getPid(), myVersion);
if (version != null) {
csv = myCodeSystemVersionDao.findByCodeSystemPidAndVersion(cs.getPid(), version);
} else if (cs.getCurrentVersion() != null) {
csv = cs.getCurrentVersion();
}
@ -1642,7 +1684,7 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc {
*/
String conceptMapUrl = termConceptMap.getUrl();
String conceptMapVersion = termConceptMap.getVersion();
Optional<TermConceptMap> optionalExistingTermConceptMapByUrl = null;
Optional<TermConceptMap> optionalExistingTermConceptMapByUrl;
if (isBlank(conceptMapVersion)) {
optionalExistingTermConceptMapByUrl = myConceptMapDao.findTermConceptMapByUrlAndNullVersion(conceptMapUrl);
} else {
@ -1776,9 +1818,10 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc {
// We have a ValueSet to pre-expand.
try {
ValueSet valueSet = txTemplate.execute(t -> {
TermValueSet refreshedValueSetToExpand = myValueSetDao.findById(valueSetToExpand.getId()).get();
TermValueSet refreshedValueSetToExpand = myValueSetDao.findById(valueSetToExpand.getId()).orElseThrow(()->new IllegalStateException("Unknown VS ID: " + valueSetToExpand.getId()));
return getValueSetFromResourceTable(refreshedValueSetToExpand.getResource());
});
assert valueSet != null;
ValueSetConceptAccumulator accumulator = new ValueSetConceptAccumulator(valueSetToExpand, myValueSetDao, myValueSetConceptDao, myValueSetConceptDesignationDao);
expandValueSet(null, valueSet, accumulator);
@ -2604,6 +2647,7 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc {
String systemVersion = theCodeSystemIdentifierType != null ? getVersionFromIdentifier(theCodeSystemIdentifierType.getValueAsString()): null;
if (theCodingType != null) {
Coding canonicalizedCoding = toCanonicalCoding(theCodingType);
assert canonicalizedCoding != null; // Shouldn't be null, since theCodingType isn't
code = canonicalizedCoding.getCode();
system = canonicalizedCoding.getSystem();
systemVersion = canonicalizedCoding.getVersion();
@ -2675,7 +2719,6 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc {
String code = theCode;
String version = theVersion;
String display = theDisplay;
if (haveCodeableConcept) {
@ -2689,7 +2732,7 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc {
}
code = nextCoding.getCode();
display = nextCoding.getDisplay();
CodeValidationResult nextValidation = codeSystemValidateCode(codeSystemUrl, version, code, display);
CodeValidationResult nextValidation = codeSystemValidateCode(codeSystemUrl, theVersion, code, display);
if (nextValidation.isOk() || i == codeableConcept.getCoding().size() - 1) {
return nextValidation;
}
@ -2705,7 +2748,7 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc {
display = coding.getDisplay();
}
return codeSystemValidateCode(codeSystemUrl, version, code, display);
return codeSystemValidateCode(codeSystemUrl, theVersion, code, display);
}

View File

@ -0,0 +1,98 @@
package ca.uhn.fhir.jpa.term;
/*-
* #%L
* HAPI FHIR JPA Server
* %%
* Copyright (C) 2014 - 2020 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import ca.uhn.fhir.util.FhirVersionIndependentConcept;
import org.apache.commons.lang3.Validate;
import org.hl7.fhir.r4.model.ValueSet;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.Collections;
import java.util.List;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
class ExpansionFilter {
public static final ExpansionFilter NO_FILTER = new ExpansionFilter(null, null);
private final String myCode;
private final String mySystem;
private final List<ValueSet.ConceptSetFilterComponent> myFilters;
private final Integer myMaxCount;
/**
* Constructor
*/
ExpansionFilter(String theSystem, String theCode) {
this(theSystem, theCode, Collections.emptyList(), null);
}
/**
* Constructor
*/
ExpansionFilter(ExpansionFilter theExpansionFilter, List<ValueSet.ConceptSetFilterComponent> theFilters, Integer theMaxCount) {
this(theExpansionFilter.getSystem(), theExpansionFilter.getCode(), theFilters, theMaxCount);
}
/**
* Constructor
*/
ExpansionFilter(@Nullable String theSystem, @Nullable String theCode, @Nonnull List<ValueSet.ConceptSetFilterComponent> theFilters, Integer theMaxCount) {
Validate.isTrue(isNotBlank(theSystem) == isNotBlank(theCode));
Validate.notNull(theFilters);
mySystem = theSystem;
myCode = theCode;
myFilters = theFilters;
myMaxCount = theMaxCount;
}
public List<ValueSet.ConceptSetFilterComponent> getFilters() {
return myFilters;
}
boolean hasCode() {
return myCode != null;
}
String getCode() {
return myCode;
}
String getSystem() {
return mySystem;
}
/**
* Converts the system/code in this filter to a FhirVersionIndependentConcept. This method
* should not be called if {@link #hasCode()} returns <code>false</code>
*/
@Nonnull
public FhirVersionIndependentConcept toFhirVersionIndependentConcept() {
Validate.isTrue(hasCode());
return new FhirVersionIndependentConcept(mySystem, myCode);
}
public Integer getMaxCount() {
return myMaxCount;
}
}

View File

@ -33,11 +33,36 @@ public interface IValueSetConceptAccumulator {
void includeConceptWithDesignations(String theSystem, String theCode, String theDisplay, @Nullable Collection<TermConceptDesignation> theDesignations);
void excludeConcept(String theSystem, String theCode);
/**
* @return Returns <code>true</code> if the code was actually present and was removed
*/
boolean excludeConcept(String theSystem, String theCode);
@Nullable
default Integer getCapacityRemaining() {
return null;
}
@Nullable
default Integer getSkipCountRemaining() {
return null;
}
@Nullable
default void consumeSkipCount(int theSkipCountToConsume) {
// nothing
}
/**
* Add or subtract from the total concept count (this is not necessarily the same thing as the number of concepts in
* the accumulator, since the <code>offset</code> and <code>count</code> parameters applied to the expansion can cause
* concepts to not actually be added.
*
* @param theAdd If <code>true</code>, increment. If <code>false</code>, decrement.
* @param theDelta The number of codes to add or subtract
*/
default void incrementOrDecrementTotalConcepts(boolean theAdd, int theDelta) {
// nothing
}
}

View File

@ -81,9 +81,9 @@ public class ValueSetConceptAccumulator implements IValueSetConceptAccumulator {
}
@Override
public void excludeConcept(String theSystem, String theCode) {
public boolean excludeConcept(String theSystem, String theCode) {
if (isAnyBlank(theSystem, theCode)) {
return;
return false;
}
// Get existing entity so it can be deleted.
@ -114,6 +114,7 @@ public class ValueSetConceptAccumulator implements IValueSetConceptAccumulator {
ourLog.info("Have excluded {} concepts from ValueSet[{}]", myConceptsExcluded, myTermValueSet.getUrl());
}
}
return false;
}
private TermValueSetConcept saveConcept(String theSystem, String theCode, String theDisplay) {

View File

@ -25,19 +25,25 @@ import ca.uhn.fhir.jpa.entity.TermConceptDesignation;
import ca.uhn.fhir.jpa.term.ex.ExpansionTooCostlyException;
import ca.uhn.fhir.model.api.annotation.Block;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.util.HapiExtensions;
import org.apache.commons.lang3.StringUtils;
import org.hl7.fhir.r4.model.StringType;
import org.hl7.fhir.r4.model.ValueSet;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
@Block()
public class ValueSetExpansionComponentWithConceptAccumulator extends ValueSet.ValueSetExpansionComponent implements IValueSetConceptAccumulator {
private final int myMaxCapacity;
private final FhirContext myContext;
private int myConceptsCount;
private int mySkipCountRemaining;
private int myHardExpansionMaximumSize;
private List<String> myMessages;
private int myAddedConcepts;
private Integer myTotalConcepts;
/**
* Constructor
@ -46,27 +52,40 @@ public class ValueSetExpansionComponentWithConceptAccumulator extends ValueSet.V
* an {@link InternalErrorException}
*/
ValueSetExpansionComponentWithConceptAccumulator(FhirContext theContext, int theMaxCapacity) {
myContext = theContext;
myMaxCapacity = theMaxCapacity;
myConceptsCount = 0;
myContext = theContext;
}
@Nullable
@Nonnull
@Override
public Integer getCapacityRemaining() {
return myMaxCapacity - myConceptsCount;
return (myMaxCapacity - myAddedConcepts) + mySkipCountRemaining;
}
public List<String> getMessages() {
if (myMessages == null) {
return Collections.emptyList();
}
return Collections.unmodifiableList(myMessages);
}
@Override
public void addMessage(String theMessage) {
addExtension()
.setUrl(HapiExtensions.EXT_VALUESET_EXPANSION_MESSAGE)
.setValue(new StringType(theMessage));
if (myMessages == null) {
myMessages = new ArrayList<>();
}
myMessages.add(theMessage);
}
@Override
public void includeConcept(String theSystem, String theCode, String theDisplay) {
if (mySkipCountRemaining > 0) {
mySkipCountRemaining--;
return;
}
incrementConceptsCount();
ValueSet.ValueSetExpansionContainsComponent contains = this.addContains();
setSystemAndVersion(theSystem, contains);
contains.setCode(theCode);
@ -75,7 +94,13 @@ public class ValueSetExpansionComponentWithConceptAccumulator extends ValueSet.V
@Override
public void includeConceptWithDesignations(String theSystem, String theCode, String theDisplay, Collection<TermConceptDesignation> theDesignations) {
if (mySkipCountRemaining > 0) {
mySkipCountRemaining--;
return;
}
incrementConceptsCount();
ValueSet.ValueSetExpansionContainsComponent contains = this.addContains();
setSystemAndVersion(theSystem, contains);
contains.setCode(theCode);
@ -95,11 +120,22 @@ public class ValueSetExpansionComponentWithConceptAccumulator extends ValueSet.V
}
@Override
public void excludeConcept(String theSystem, String theCode) {
public void consumeSkipCount(int theSkipCountToConsume) {
mySkipCountRemaining -= theSkipCountToConsume;
}
@Nullable
@Override
public Integer getSkipCountRemaining() {
return mySkipCountRemaining;
}
@Override
public boolean excludeConcept(String theSystem, String theCode) {
String excludeSystem;
String excludeSystemVersion;
int versionSeparator = theSystem.indexOf("|");
if(versionSeparator > -1) {
if (versionSeparator > -1) {
excludeSystemVersion = theSystem.substring(versionSeparator + 1);
excludeSystem = theSystem.substring(0, versionSeparator);
} else {
@ -107,22 +143,47 @@ public class ValueSetExpansionComponentWithConceptAccumulator extends ValueSet.V
excludeSystemVersion = null;
}
if (excludeSystemVersion != null) {
this.getContains().removeIf(t ->
return this.getContains().removeIf(t ->
excludeSystem.equals(t.getSystem()) &&
theCode.equals(t.getCode()) &&
excludeSystemVersion.equals(t.getVersion()));
theCode.equals(t.getCode()) &&
excludeSystemVersion.equals(t.getVersion()));
} else {
this.getContains().removeIf(t ->
theSystem.equals(t.getSystem()) &&
theCode.equals(t.getCode()));
return this.getContains().removeIf(t ->
theSystem.equals(t.getSystem()) &&
theCode.equals(t.getCode()));
}
}
private void incrementConceptsCount() {
if (++myConceptsCount > myMaxCapacity) {
Integer capacityRemaining = getCapacityRemaining();
if (capacityRemaining == 0) {
String msg = myContext.getLocalizer().getMessage(BaseTermReadSvcImpl.class, "expansionTooLarge", myMaxCapacity);
throw new ExpansionTooCostlyException(msg);
}
if (myHardExpansionMaximumSize > 0 && myAddedConcepts > myHardExpansionMaximumSize) {
String msg = myContext.getLocalizer().getMessage(BaseTermReadSvcImpl.class, "expansionTooLarge", myHardExpansionMaximumSize);
throw new ExpansionTooCostlyException(msg);
}
myAddedConcepts++;
}
public Integer getTotalConcepts() {
return myTotalConcepts;
}
@Override
public void incrementOrDecrementTotalConcepts(boolean theAdd, int theDelta) {
int delta = theDelta;
if (!theAdd) {
delta = -delta;
}
if (myTotalConcepts == null) {
myTotalConcepts = delta;
} else {
myTotalConcepts = myTotalConcepts + delta;
}
}
private void setSystemAndVersion(String theSystemAndVersion, ValueSet.ValueSetExpansionContainsComponent myComponent) {
@ -130,11 +191,18 @@ public class ValueSetExpansionComponentWithConceptAccumulator extends ValueSet.V
int versionSeparator = theSystemAndVersion.lastIndexOf('|');
if (versionSeparator != -1) {
myComponent.setVersion(theSystemAndVersion.substring(versionSeparator + 1));
myComponent.setSystem(theSystemAndVersion.substring(0,versionSeparator));
myComponent.setSystem(theSystemAndVersion.substring(0, versionSeparator));
} else {
myComponent.setSystem(theSystemAndVersion);
}
}
}
public void setSkipCountRemaining(int theSkipCountRemaining) {
mySkipCountRemaining = theSkipCountRemaining;
}
public void setHardExpansionMaximumSize(int theHardExpansionMaximumSize) {
myHardExpansionMaximumSize = theHardExpansionMaximumSize;
}
}

View File

@ -32,7 +32,9 @@ import org.springframework.test.util.AopTestUtils;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.stream.Collectors;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.fail;
@ -100,7 +102,7 @@ public class FhirResourceDaoDstu3ValidateTest extends BaseJpaDstu3Test {
@Test
public void testValidateQuestionnaireResponseWithValueSetIncludingCompleteCodeSystem() throws IOException {
public void testExpandLocalCodeSystemWithExplicitCodes() throws IOException {
CodeSystem cs = loadResourceFromClasspath(CodeSystem.class, "/dstu3/iar/CodeSystem-iar-citizenship-status.xml");
myCodeSystemDao.create(cs);
@ -110,18 +112,9 @@ public class FhirResourceDaoDstu3ValidateTest extends BaseJpaDstu3Test {
ValueSet expansion = myValueSetDao.expandByIdentifier("http://ccim.on.ca/fhir/iar/ValueSet/iar-citizenship-status", null);
ourLog.info(myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(expansion));
// Questionnaire q = loadResourceFromClasspath(Questionnaire.class,"/dstu3/iar/Questionnaire-iar-test.xml" );
// myQuestionnaireDao.create(q);
//
//
//
// Bundle bundleForValidation = loadResourceFromClasspath(Bundle.class, "/dstu3/iar/Bundle-for-validation.xml");
// try {
// MethodOutcome outcome = myBundleDao.validate(bundleForValidation, null, null, null, null, null, null);
// ourLog.info(myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(outcome.getOperationOutcome()));
// } catch (PreconditionFailedException e) {
// ourLog.info(myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(e.getOperationOutcome()));
// }
assertThat(expansion.getExpansion().getContains().stream().map(t->t.getCode()).collect(Collectors.toList()), containsInAnyOrder(
"CDN", "PR", "TR", "REF", "UNK", "ASKU"
));
}

View File

@ -1015,6 +1015,7 @@ public class FhirResourceDaoR4TerminologyTest extends BaseJpaR4Test {
myAllergyIntoleranceDao.create(ai3, mySrd).getId().toUnqualifiedVersionless().getValue();
SearchParameterMap params;
params = new SearchParameterMap();
params.add(AllergyIntolerance.SP_CLINICAL_STATUS, new TokenParam(null, "active"));
assertThat(toUnqualifiedVersionlessIdValues(myAllergyIntoleranceDao.search(params)), containsInAnyOrder(id1));

View File

@ -10,8 +10,11 @@ import ca.uhn.fhir.jpa.entity.TermValueSetConcept;
import ca.uhn.fhir.jpa.entity.TermValueSetConceptDesignation;
import ca.uhn.fhir.jpa.entity.TermValueSetPreExpansionStatusEnum;
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
import ca.uhn.fhir.jpa.model.util.JpaConstants;
import ca.uhn.fhir.jpa.term.custom.CustomTerminologySet;
import ca.uhn.fhir.jpa.util.SqlQuery;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.util.HapiExtensions;
import com.google.common.collect.Lists;
import org.hl7.fhir.instance.model.api.IIdType;
@ -31,9 +34,12 @@ import javax.annotation.Nonnull;
import java.io.IOException;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.empty;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
@ -87,6 +93,260 @@ public class ValueSetExpansionR4Test extends BaseTermR4Test {
assertEquals(24, expandedValueSet.getExpansion().getContains().size());
}
@Test
public void testExpandInline_IncludeCodeSystem_FilterOnDisplay_NoFilter() throws Exception {
loadAndPersistCodeSystemWithDesignations(HttpVerb.PUT);
ValueSet input = new ValueSet();
input.getCompose()
.addInclude()
.setSystem("http://acme.org");
ValueSet expandedValueSet = myTermSvc.expandValueSet(new ValueSetExpansionOptions(), input);
ourLog.info("Expanded ValueSet:\n" + myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(expandedValueSet));
assertEquals(24, expandedValueSet.getExpansion().getTotal());
}
@Test
public void testExpandInline_IncludeCodeSystem_FilterOnDisplay_ExactFilter() throws Exception {
loadAndPersistCodeSystemWithDesignations(HttpVerb.PUT);
ValueSet input = new ValueSet();
input.getCompose()
.addInclude()
.setSystem("http://acme.org")
.addFilter()
.setProperty(JpaConstants.VALUESET_FILTER_DISPLAY)
.setOp(ValueSet.FilterOperator.EQUAL)
.setValue("Systolic blood pressure--inspiration");
ValueSet expandedValueSet = myTermSvc.expandValueSet(new ValueSetExpansionOptions(), input);
ourLog.info("Expanded ValueSet:\n" + myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(expandedValueSet));
assertEquals(1, expandedValueSet.getExpansion().getTotal());
assertThat(expandedValueSet.getExpansion().getContains().stream().map(t -> t.getDisplay()).collect(Collectors.toList()), containsInAnyOrder(
"Systolic blood pressure--inspiration"
));
}
@Test
public void testExpandInline_IncludeCodeSystem_FilterOnDisplay_LeftMatchFilter() throws Exception {
loadAndPersistCodeSystemWithDesignations(HttpVerb.PUT);
ValueSet input = new ValueSet();
input.getCompose()
.addInclude()
.setSystem("http://acme.org")
.addFilter()
.setProperty(JpaConstants.VALUESET_FILTER_DISPLAY)
.setOp(ValueSet.FilterOperator.EQUAL)
.setValue("Systolic blood pressure 1");
ValueSet expandedValueSet = myTermSvc.expandValueSet(new ValueSetExpansionOptions(), input);
ourLog.info("Expanded ValueSet:\n" + myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(expandedValueSet));
assertEquals(3, expandedValueSet.getExpansion().getTotal());
assertThat(expandedValueSet.getExpansion().getContains().stream().map(t -> t.getDisplay()).collect(Collectors.toList()), containsInAnyOrder(
"Systolic blood pressure 1 hour minimum",
"Systolic blood pressure 1 hour mean",
"Systolic blood pressure 1 hour maximum"
));
}
@Test
public void testExpandInline_IncludePreExpandedValueSetByUri_FilterOnDisplay_LeftMatch_SelectAll() {
myDaoConfig.setPreExpandValueSets(true);
create100ConceptsCodeSystemAndValueSet();
ValueSet input = new ValueSet();
input.getCompose()
.addInclude()
.addValueSet("http://foo/vs")
.addFilter()
.setProperty(JpaConstants.VALUESET_FILTER_DISPLAY)
.setOp(ValueSet.FilterOperator.EQUAL)
.setValue("display value 9");
myCaptureQueriesListener.clear();
ValueSet expandedValueSet = myTermSvc.expandValueSet(new ValueSetExpansionOptions(), input);
ourLog.debug("Expanded ValueSet:\n" + myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(expandedValueSet));
assertThat(toCodes(expandedValueSet).toString(), toCodes(expandedValueSet), contains(
"code9", "code90", "code91", "code92", "code93", "code94", "code95", "code96", "code97", "code98", "code99"
));
assertEquals(11, expandedValueSet.getExpansion().getContains().size(), toCodes(expandedValueSet).toString());
assertEquals(11, expandedValueSet.getExpansion().getTotal());
// Make sure we used the pre-expanded version
List<SqlQuery> selectQueries = myCaptureQueriesListener.getSelectQueries();
String lastSelectQuery = selectQueries.get(selectQueries.size() - 1).getSql(true, true).toLowerCase();
assertThat(lastSelectQuery, containsString("concept_display like 'display value 9%'"));
}
@Test
public void testExpandInline_IncludePreExpandedValueSetByUri_FilterOnDisplay_LeftMatch_SelectRange() {
myDaoConfig.setPreExpandValueSets(true);
create100ConceptsCodeSystemAndValueSet();
ValueSet input = new ValueSet();
input.getCompose()
.addInclude()
.addValueSet("http://foo/vs")
.addFilter()
.setProperty(JpaConstants.VALUESET_FILTER_DISPLAY)
.setOp(ValueSet.FilterOperator.EQUAL)
.setValue("display value 9");
myCaptureQueriesListener.clear();
ValueSetExpansionOptions expansionOptions = new ValueSetExpansionOptions()
.setOffset(3)
.setCount(4);
ValueSet expandedValueSet = myTermSvc.expandValueSet(expansionOptions, input);
ourLog.debug("Expanded ValueSet:\n" + myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(expandedValueSet));
assertThat(toCodes(expandedValueSet).toString(), toCodes(expandedValueSet), contains(
"code92", "code93", "code94", "code95"
));
assertEquals(4, expandedValueSet.getExpansion().getContains().size(), toCodes(expandedValueSet).toString());
assertEquals(11, expandedValueSet.getExpansion().getTotal());
// Make sure we used the pre-expanded version
List<SqlQuery> selectQueries = myCaptureQueriesListener.getSelectQueries();
String lastSelectQuery = selectQueries.get(selectQueries.size() - 1).getSql(true, true).toLowerCase();
assertThat(lastSelectQuery, containsString("concept_display like 'display value 9%'"));
}
@Test
public void testExpandInline_IncludePreExpandedValueSetByUri_ExcludeCodes_FilterOnDisplay_LeftMatch_SelectAll() {
myDaoConfig.setPreExpandValueSets(true);
create100ConceptsCodeSystemAndValueSet();
ValueSet input = new ValueSet();
input.getCompose()
.addInclude()
.addValueSet("http://foo/vs")
.addFilter()
.setProperty(JpaConstants.VALUESET_FILTER_DISPLAY)
.setOp(ValueSet.FilterOperator.EQUAL)
.setValue("display value 9");
input.getCompose()
.addExclude()
.addValueSet("http://foo/vs")
.addFilter()
.setProperty(JpaConstants.VALUESET_FILTER_DISPLAY)
.setOp(ValueSet.FilterOperator.EQUAL)
.setValue("display value 90");
myCaptureQueriesListener.clear();
ValueSet expandedValueSet = myTermSvc.expandValueSet(new ValueSetExpansionOptions(), input);
ourLog.debug("Expanded ValueSet:\n" + myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(expandedValueSet));
assertThat(toCodes(expandedValueSet).toString(), toCodes(expandedValueSet), contains(
"code9", "code91", "code92", "code93", "code94", "code95", "code96", "code97", "code98", "code99"
));
assertEquals(10, expandedValueSet.getExpansion().getContains().size(), toCodes(expandedValueSet).toString());
assertEquals(10, expandedValueSet.getExpansion().getTotal());
// Make sure we used the pre-expanded version
List<SqlQuery> selectQueries = myCaptureQueriesListener.getSelectQueries();
String lastSelectQuery = selectQueries.get(selectQueries.size() - 1).getSql(true, true).toLowerCase();
assertThat(lastSelectQuery, containsString("concept_display like 'display value 90%'"));
}
@Test
public void testExpandInline_IncludePreExpandedValueSetByUri_ExcludeCodes_FilterOnDisplay_LeftMatch_SelectRange() {
myDaoConfig.setPreExpandValueSets(true);
create100ConceptsCodeSystemAndValueSet();
ValueSet input = new ValueSet();
input.getCompose()
.addInclude()
.addValueSet("http://foo/vs")
.addFilter()
.setProperty(JpaConstants.VALUESET_FILTER_DISPLAY)
.setOp(ValueSet.FilterOperator.EQUAL)
.setValue("display value 9");
input.getCompose()
.addExclude()
.addValueSet("http://foo/vs")
.addFilter()
.setProperty(JpaConstants.VALUESET_FILTER_DISPLAY)
.setOp(ValueSet.FilterOperator.EQUAL)
.setValue("display value 90");
myCaptureQueriesListener.clear();
ValueSetExpansionOptions options = new ValueSetExpansionOptions();
options.setOffset(3);
options.setCount(4);
try {
myTermSvc.expandValueSet(options, input);
fail();
} catch (InvalidRequestException e) {
assertEquals("ValueSet expansion can not combine \"offset\" with \"ValueSet.compose.exclude\" unless the ValueSet has been pre-expanded. ValueSet \"Unidentified ValueSet\" must be pre-expanded for this operation to work.", e.getMessage());
}
}
public void create100ConceptsCodeSystemAndValueSet() {
CodeSystem cs = new CodeSystem();
cs.setUrl("http://foo/cs");
cs.setContent(CodeSystem.CodeSystemContentMode.NOTPRESENT);
myCodeSystemDao.create(cs);
CustomTerminologySet additions = new CustomTerminologySet();
for (int i = 0; i < 100; i++) {
additions.addRootConcept("code" + i, "display value " + i);
}
myTermCodeSystemStorageSvc.applyDeltaCodeSystemsAdd("http://foo/cs", additions);
ValueSet vs = new ValueSet();
vs.setUrl("http://foo/vs");
vs.getCompose().addInclude().setSystem("http://foo/cs");
myValueSetDao.create(vs);
myTermSvc.preExpandDeferredValueSetsToTerminologyTables();
}
@Test
public void testExpandInline_IncludeNonPreExpandedValueSetByUri_FilterOnDisplay_LeftMatch() {
myDaoConfig.setPreExpandValueSets(true);
create100ConceptsCodeSystemAndValueSet();
ValueSet input = new ValueSet();
input.getCompose()
.addInclude()
.addValueSet("http://foo/vs")
.addFilter()
.setProperty(JpaConstants.VALUESET_FILTER_DISPLAY)
.setOp(ValueSet.FilterOperator.EQUAL)
.setValue("display value 9");
myCaptureQueriesListener.clear();
ValueSet expandedValueSet = myTermSvc.expandValueSet(new ValueSetExpansionOptions(), input);
ourLog.debug("Expanded ValueSet:\n" + myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(expandedValueSet));
assertThat(toCodes(expandedValueSet).toString(), toCodes(expandedValueSet), contains(
"code9", "code90", "code91", "code92", "code93", "code94", "code95", "code96", "code97", "code98", "code99"
));
assertEquals(11, expandedValueSet.getExpansion().getContains().size(), toCodes(expandedValueSet).toString());
assertEquals(11, expandedValueSet.getExpansion().getTotal(), toCodes(expandedValueSet).toString());
// Make sure we used the pre-expanded version
List<SqlQuery> selectQueries = myCaptureQueriesListener.getSelectQueries();
String lastSelectQuery = selectQueries.get(selectQueries.size() - 1).getSql(true, true).toLowerCase();
assertThat(lastSelectQuery, containsString("concept_display like 'display value 9%'"));
}
@Nonnull
public List<String> toCodes(ValueSet theExpandedValueSet) {
return theExpandedValueSet.getExpansion().getContains().stream().map(t -> t.getCode()).collect(Collectors.toList());
}
@SuppressWarnings("SpellCheckingInspection")
@Test
public void testExpandTermValueSetAndChildren() throws Exception {
@ -115,11 +375,7 @@ public class ValueSetExpansionR4Test extends BaseTermR4Test {
assertEquals(codeSystem.getConcept().size(), expandedValueSet.getExpansion().getTotal());
assertEquals(myDaoConfig.getPreExpandValueSetsDefaultOffset(), expandedValueSet.getExpansion().getOffset());
assertEquals(2, expandedValueSet.getExpansion().getParameter().size());
assertEquals("offset", expandedValueSet.getExpansion().getParameter().get(0).getName());
assertEquals(0, expandedValueSet.getExpansion().getParameter().get(0).getValueIntegerType().getValue().intValue());
assertEquals("count", expandedValueSet.getExpansion().getParameter().get(1).getName());
assertEquals(1000, expandedValueSet.getExpansion().getParameter().get(1).getValueIntegerType().getValue().intValue());
assertEquals(0, expandedValueSet.getExpansion().getParameter().size());
assertEquals(codeSystem.getConcept().size(), expandedValueSet.getExpansion().getContains().size());
@ -337,11 +593,7 @@ public class ValueSetExpansionR4Test extends BaseTermR4Test {
assertEquals(codeSystem.getConcept().size(), expandedValueSet.getExpansion().getTotal());
assertEquals(myDaoConfig.getPreExpandValueSetsDefaultOffset(), expandedValueSet.getExpansion().getOffset());
assertEquals(2, expandedValueSet.getExpansion().getParameter().size());
assertEquals("offset", expandedValueSet.getExpansion().getParameter().get(0).getName());
assertEquals(0, expandedValueSet.getExpansion().getParameter().get(0).getValueIntegerType().getValue().intValue());
assertEquals("count", expandedValueSet.getExpansion().getParameter().get(1).getName());
assertEquals(1000, expandedValueSet.getExpansion().getParameter().get(1).getValueIntegerType().getValue().intValue());
assertEquals(0, expandedValueSet.getExpansion().getParameter().size());
assertEquals(codeSystem.getConcept().size(), expandedValueSet.getExpansion().getContains().size());
@ -796,7 +1048,7 @@ public class ValueSetExpansionR4Test extends BaseTermR4Test {
String encoded = myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(outcome);
ourLog.info(encoded);
Extension extensionByUrl = outcome.getExpansion().getExtensionByUrl(HapiExtensions.EXT_VALUESET_EXPANSION_MESSAGE);
Extension extensionByUrl = outcome.getMeta().getExtensionByUrl(HapiExtensions.EXT_VALUESET_EXPANSION_MESSAGE);
assertEquals("Unknown CodeSystem URI \"http://unknown-system\" referenced from ValueSet", extensionByUrl.getValueAsPrimitive().getValueAsString());
}
@ -877,8 +1129,8 @@ public class ValueSetExpansionR4Test extends BaseTermR4Test {
ValueSet.ConceptSetComponent include = vs.getCompose().addInclude();
include.setSystem(CS_URL);
try {
myTermSvc.expandValueSet(null, vs);
fail();
ValueSet expansion = myTermSvc.expandValueSet(null, vs);
fail("Expanded to " + expansion.getExpansion().getContains().size() + " but max was " + myDaoConfig.getMaximumExpansionSize());
} catch (InternalErrorException e) {
assertEquals("Expansion of ValueSet produced too many codes (maximum 50) - Operation aborted!", e.getMessage());
}

View File

@ -134,7 +134,7 @@ public class HapiFhirJpaMigrationTasks extends BaseMigrationTasks<VersionEnum> {
init510_20200706_to_20200714();
Builder.BuilderWithTableName empiLink = version.onTable("MPI_LINK");
empiLink.addColumn("20200715.1", "VERSION").nonNullable().type(ColumnTypeEnum.STRING, EmpiLink.VERSION_LENGTH);
empiLink.addColumn("20200715.1", "VERSION").nonNullable().type(ColumnTypeEnum.STRING, 16);
empiLink.addColumn("20200715.2", "EID_MATCH").nullable().type(ColumnTypeEnum.BOOLEAN);
empiLink.addColumn("20200715.3", "NEW_PERSON").nullable().type(ColumnTypeEnum.BOOLEAN);
empiLink.addColumn("20200715.4", "VECTOR").nullable().type(ColumnTypeEnum.LONG);
@ -143,11 +143,11 @@ public class HapiFhirJpaMigrationTasks extends BaseMigrationTasks<VersionEnum> {
init510_20200725();
//EMPI Target Type
empiLink.addColumn("20200727.1","TARGET_TYPE").nullable().type(ColumnTypeEnum.STRING, EmpiLink.TARGET_TYPE_LENGTH);
empiLink.addColumn("20200727.1","TARGET_TYPE").nullable().type(ColumnTypeEnum.STRING, 40);
//ConceptMap add version for search
Builder.BuilderWithTableName trmConceptMap = version.onTable("TRM_CONCEPT_MAP");
trmConceptMap.addColumn("20200910.1", "VER").nullable().type(ColumnTypeEnum.STRING, TermConceptMap.MAX_VER_LENGTH);
trmConceptMap.addColumn("20200910.1", "VER").nullable().type(ColumnTypeEnum.STRING, 200);
trmConceptMap.dropIndex("20200910.2", "IDX_CONCEPT_MAP_URL");
trmConceptMap.addIndex("20200910.3", "IDX_CONCEPT_MAP_URL").unique(true).withColumns("URL", "VER");
@ -155,15 +155,15 @@ public class HapiFhirJpaMigrationTasks extends BaseMigrationTasks<VersionEnum> {
Builder.BuilderWithTableName trmCodeSystemVer = version.onTable("TRM_CODESYSTEM_VER");
trmCodeSystemVer.addIndex("20200923.1", "IDX_CODESYSTEM_AND_VER").unique(true).withColumns("CODESYSTEM_PID", "CS_VERSION_ID");
Builder.BuilderWithTableName trmValueSet = version.onTable("TRM_VALUESET");
trmValueSet.addColumn("20200923.2", "VER").nullable().type(ColumnTypeEnum.STRING, TermValueSet.MAX_VER_LENGTH);
trmValueSet.addColumn("20200923.2", "VER").nullable().type(ColumnTypeEnum.STRING, 200);
trmValueSet.dropIndex("20200923.3", "IDX_VALUESET_URL");
trmValueSet.addIndex("20200923.4", "IDX_VALUESET_URL").unique(true).withColumns("URL", "VER");
//Term ValueSet Component add system version
Builder.BuilderWithTableName trmValueSetComp = version.onTable("TRM_VALUESET_CONCEPT");
trmValueSetComp.addColumn("20201028.1", "SYSTEM_VER").nullable().type(ColumnTypeEnum.STRING, TermCodeSystemVersion.MAX_VERSION_LENGTH);
trmValueSetComp.addColumn("20201028.1", "SYSTEM_VER").nullable().type(ColumnTypeEnum.STRING, 200);
trmValueSetComp.dropIndex("20201028.2", "IDX_VS_CONCEPT_CS_CD");
trmValueSetComp.addIndex("20201028.3", "IDX_VS_CONCEPT_CS_CD").unique(true).withColumns("VALUESET_PID", "SYSTEM_URL", "SYSTEM_VER", "CODEVAL");
trmValueSetComp.addIndex("20201028.3", "IDX_VS_CONCEPT_CS_CODE").unique(true).withColumns("VALUESET_PID", "SYSTEM_URL", "SYSTEM_VER", "CODEVAL");
}
protected void init510_20200725() {

View File

@ -202,6 +202,7 @@ public class JpaConstants {
* URL for extension on a Phonetic String SearchParameter indicating that text values should be phonetically indexed with the named encoder
*/
public static final String EXT_SEARCHPARAM_PHONETIC_ENCODER = "http://hapifhir.io/fhir/StructureDefinition/searchparameter-phonetic-encoder";
public static final String VALUESET_FILTER_DISPLAY = "display";
/**
* Non-instantiable