Merge pull request #2421 from hapifhir/kh-20200222-search-list
Kh 20200222 search list
This commit is contained in:
commit
60b462540f
|
@ -188,6 +188,7 @@ public class Constants {
|
||||||
public static final String PARAM_SOURCE = "_source";
|
public static final String PARAM_SOURCE = "_source";
|
||||||
public static final String PARAM_SUMMARY = "_summary";
|
public static final String PARAM_SUMMARY = "_summary";
|
||||||
public static final String PARAM_TAG = "_tag";
|
public static final String PARAM_TAG = "_tag";
|
||||||
|
public static final String PARAM_LIST = "_list";
|
||||||
public static final String PARAM_TAGS = "_tags";
|
public static final String PARAM_TAGS = "_tags";
|
||||||
public static final String PARAM_TEXT = "_text";
|
public static final String PARAM_TEXT = "_text";
|
||||||
public static final String PARAM_VALIDATE = "_validate";
|
public static final String PARAM_VALIDATE = "_validate";
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
---
|
||||||
|
type: add
|
||||||
|
issue: 2417
|
||||||
|
title: "Support for the FHIR _list search parameter."
|
|
@ -57,6 +57,7 @@ import ca.uhn.fhir.jpa.searchparam.MatchUrlService;
|
||||||
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
|
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
|
||||||
import ca.uhn.fhir.jpa.util.JpaInterceptorBroadcaster;
|
import ca.uhn.fhir.jpa.util.JpaInterceptorBroadcaster;
|
||||||
import ca.uhn.fhir.model.api.IQueryParameterType;
|
import ca.uhn.fhir.model.api.IQueryParameterType;
|
||||||
|
import ca.uhn.fhir.model.dstu2.resource.ListResource;
|
||||||
import ca.uhn.fhir.model.primitive.IdDt;
|
import ca.uhn.fhir.model.primitive.IdDt;
|
||||||
import ca.uhn.fhir.rest.api.CacheControlDirective;
|
import ca.uhn.fhir.rest.api.CacheControlDirective;
|
||||||
import ca.uhn.fhir.rest.api.Constants;
|
import ca.uhn.fhir.rest.api.Constants;
|
||||||
|
@ -73,6 +74,7 @@ import ca.uhn.fhir.rest.api.server.SimplePreResourceAccessDetails;
|
||||||
import ca.uhn.fhir.rest.api.server.SimplePreResourceShowDetails;
|
import ca.uhn.fhir.rest.api.server.SimplePreResourceShowDetails;
|
||||||
import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId;
|
import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId;
|
||||||
import ca.uhn.fhir.rest.api.server.storage.TransactionDetails;
|
import ca.uhn.fhir.rest.api.server.storage.TransactionDetails;
|
||||||
|
import ca.uhn.fhir.rest.param.HasParam;
|
||||||
import ca.uhn.fhir.rest.server.IPagingProvider;
|
import ca.uhn.fhir.rest.server.IPagingProvider;
|
||||||
import ca.uhn.fhir.rest.server.IRestfulServerDefaults;
|
import ca.uhn.fhir.rest.server.IRestfulServerDefaults;
|
||||||
import ca.uhn.fhir.rest.server.RestfulServerUtils;
|
import ca.uhn.fhir.rest.server.RestfulServerUtils;
|
||||||
|
@ -120,12 +122,12 @@ import javax.annotation.PostConstruct;
|
||||||
import javax.persistence.NoResultException;
|
import javax.persistence.NoResultException;
|
||||||
import javax.persistence.TypedQuery;
|
import javax.persistence.TypedQuery;
|
||||||
import javax.servlet.http.HttpServletResponse;
|
import javax.servlet.http.HttpServletResponse;
|
||||||
import javax.validation.constraints.NotNull;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
|
import java.util.Iterator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
@ -1301,7 +1303,54 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Notify interceptors
|
translateSearchParams(theParams);
|
||||||
|
|
||||||
|
notifySearchInterceptors(theParams, theRequest);
|
||||||
|
|
||||||
|
CacheControlDirective cacheControlDirective = new CacheControlDirective();
|
||||||
|
if (theRequest != null) {
|
||||||
|
cacheControlDirective.parse(theRequest.getHeaders(Constants.HEADER_CACHE_CONTROL));
|
||||||
|
}
|
||||||
|
|
||||||
|
RequestPartitionId requestPartitionId = myPartitionHelperSvc.determineReadPartitionForRequest(theRequest, getResourceName());
|
||||||
|
IBundleProvider retVal = mySearchCoordinatorSvc.registerSearch(this, theParams, getResourceName(), cacheControlDirective, theRequest, requestPartitionId);
|
||||||
|
|
||||||
|
if (retVal instanceof PersistedJpaBundleProvider) {
|
||||||
|
PersistedJpaBundleProvider provider = (PersistedJpaBundleProvider) retVal;
|
||||||
|
if (provider.getCacheStatus() == SearchCacheStatusEnum.HIT) {
|
||||||
|
if (theServletResponse != null && theRequest != null) {
|
||||||
|
String value = "HIT from " + theRequest.getFhirServerBase();
|
||||||
|
theServletResponse.addHeader(Constants.HEADER_X_CACHE, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return retVal;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void translateSearchParams(SearchParameterMap theParams) {
|
||||||
|
Iterator<String> keyIterator = theParams.keySet().iterator();
|
||||||
|
|
||||||
|
// Translate _list=42 to _has=List:item:_id=42
|
||||||
|
while (keyIterator.hasNext()) {
|
||||||
|
String key = keyIterator.next();
|
||||||
|
if (Constants.PARAM_LIST.equals((key))) {
|
||||||
|
List<List<IQueryParameterType>> andOrValues = theParams.get(key);
|
||||||
|
theParams.remove(key);
|
||||||
|
List<List<IQueryParameterType>> hasParamValues = new ArrayList<>();
|
||||||
|
for (List<IQueryParameterType> orValues : andOrValues) {
|
||||||
|
List<IQueryParameterType> orList = new ArrayList<>();
|
||||||
|
for (IQueryParameterType value : orValues) {
|
||||||
|
orList.add(new HasParam("List", ListResource.SP_ITEM, ListResource.SP_RES_ID, value.getValueAsQueryToken(null)));
|
||||||
|
}
|
||||||
|
hasParamValues.add(orList);
|
||||||
|
}
|
||||||
|
theParams.put(Constants.PARAM_HAS, hasParamValues);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void notifySearchInterceptors(SearchParameterMap theParams, RequestDetails theRequest) {
|
||||||
if (theRequest != null) {
|
if (theRequest != null) {
|
||||||
ActionRequestDetails requestDetails = new ActionRequestDetails(theRequest, getContext(), getResourceName(), null);
|
ActionRequestDetails requestDetails = new ActionRequestDetails(theRequest, getContext(), getResourceName(), null);
|
||||||
notifyInterceptors(RestOperationTypeEnum.SEARCH_TYPE, requestDetails);
|
notifyInterceptors(RestOperationTypeEnum.SEARCH_TYPE, requestDetails);
|
||||||
|
@ -1335,26 +1384,6 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
|
||||||
theParams.setCount(theRequest.getServer().getDefaultPageSize());
|
theParams.setCount(theRequest.getServer().getDefaultPageSize());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
CacheControlDirective cacheControlDirective = new CacheControlDirective();
|
|
||||||
if (theRequest != null) {
|
|
||||||
cacheControlDirective.parse(theRequest.getHeaders(Constants.HEADER_CACHE_CONTROL));
|
|
||||||
}
|
|
||||||
|
|
||||||
RequestPartitionId requestPartitionId = myPartitionHelperSvc.determineReadPartitionForRequest(theRequest, getResourceName());
|
|
||||||
IBundleProvider retVal = mySearchCoordinatorSvc.registerSearch(this, theParams, getResourceName(), cacheControlDirective, theRequest, requestPartitionId);
|
|
||||||
|
|
||||||
if (retVal instanceof PersistedJpaBundleProvider) {
|
|
||||||
PersistedJpaBundleProvider provider = (PersistedJpaBundleProvider) retVal;
|
|
||||||
if (provider.getCacheStatus() == SearchCacheStatusEnum.HIT) {
|
|
||||||
if (theServletResponse != null && theRequest != null) {
|
|
||||||
String value = "HIT from " + theRequest.getFhirServerBase();
|
|
||||||
theServletResponse.addHeader(Constants.HEADER_X_CACHE, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return retVal;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -0,0 +1,135 @@
|
||||||
|
package ca.uhn.fhir.jpa.dao.r4;
|
||||||
|
|
||||||
|
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
|
||||||
|
import ca.uhn.fhir.jpa.searchparam.MatchUrlService;
|
||||||
|
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
|
||||||
|
import ca.uhn.fhir.rest.api.server.IBundleProvider;
|
||||||
|
import ca.uhn.fhir.rest.param.HasParam;
|
||||||
|
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||||
|
import org.hl7.fhir.instance.model.api.IIdType;
|
||||||
|
import org.hl7.fhir.r4.model.ListResource;
|
||||||
|
import org.hl7.fhir.r4.model.Patient;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import static ca.uhn.fhir.rest.api.Constants.PARAM_HAS;
|
||||||
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
|
import static org.hamcrest.Matchers.hasSize;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
public class FhirResourceDaoSearchListTest extends BaseJpaR4Test {
|
||||||
|
@Autowired
|
||||||
|
@Qualifier("myListDaoR4")
|
||||||
|
protected IFhirResourceDao<ListResource> myListResourceDao;
|
||||||
|
@Autowired
|
||||||
|
private MatchUrlService myMatchUrlService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See https://www.hl7.org/fhir/search.html#list
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void testBasicListQuery() {
|
||||||
|
IIdType[] patientIds = createPatients(2);
|
||||||
|
String listIdString = createList(patientIds);
|
||||||
|
|
||||||
|
String queryString = "_list=" + listIdString;
|
||||||
|
testQuery(queryString, patientIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testBigListQuery() {
|
||||||
|
IIdType[] patientIds = createPatients(100);
|
||||||
|
String listIdString = createList(patientIds);
|
||||||
|
|
||||||
|
String queryString = "_list=" + listIdString;
|
||||||
|
testQuery(queryString, patientIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void testQuery(String theQueryString, IIdType... theExpectedPatientIds) {
|
||||||
|
SearchParameterMap map = myMatchUrlService.translateMatchUrl(theQueryString, myFhirCtx.getResourceDefinition("List"));
|
||||||
|
IBundleProvider bundle = myPatientDao.search(map);
|
||||||
|
List<IBaseResource> resources = bundle.getResources(0, theExpectedPatientIds.length);
|
||||||
|
assertThat(resources, hasSize(theExpectedPatientIds.length));
|
||||||
|
|
||||||
|
Set<IIdType> ids = resources.stream().map(IBaseResource::getIdElement).collect(Collectors.toSet());
|
||||||
|
assertThat(ids, hasSize(theExpectedPatientIds.length));
|
||||||
|
|
||||||
|
for(IIdType patientId: theExpectedPatientIds) {
|
||||||
|
assertTrue(ids.contains(patientId));
|
||||||
|
|
||||||
|
//assertThat(patientId, contains(ids));
|
||||||
|
}
|
||||||
|
// assert ids equal pid1 and pid2
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testAnd() {
|
||||||
|
IIdType[] patientIds = createPatients(3);
|
||||||
|
String listIdString1 = createList(patientIds[0], patientIds[1]);
|
||||||
|
String listIdString2 = createList(patientIds[1], patientIds[2]);
|
||||||
|
|
||||||
|
String queryString = "_list=" + listIdString1 + "&_list=" + listIdString2;
|
||||||
|
|
||||||
|
testQuery(queryString, patientIds[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testOr() {
|
||||||
|
IIdType[] patientIds = createPatients(3);
|
||||||
|
String listIdString1 = createList(patientIds[0], patientIds[1]);
|
||||||
|
String listIdString2 = createList(patientIds[1], patientIds[2]);
|
||||||
|
|
||||||
|
String queryString = "_list=" + listIdString1 + "," + listIdString2;
|
||||||
|
testQuery(queryString, patientIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testBoth() {
|
||||||
|
IIdType[] patientIds = createPatients(5);
|
||||||
|
String listIdString1 = createList(patientIds[0], patientIds[1]);
|
||||||
|
String listIdString2 = createList(patientIds[3], patientIds[4]);
|
||||||
|
String listIdString3 = createList(patientIds[2], patientIds[3]);
|
||||||
|
|
||||||
|
String queryString = "_list=" + listIdString1 + "," + listIdString2 + "&_list=" + listIdString3;
|
||||||
|
testQuery(queryString, patientIds[3]);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testAlternateSyntax() {
|
||||||
|
IIdType[] patientIds = createPatients(2);
|
||||||
|
String listIdString = createList(patientIds);
|
||||||
|
|
||||||
|
// What we need to emulate
|
||||||
|
// /Patient?_has=List:item:_id=123
|
||||||
|
SearchParameterMap map = SearchParameterMap.newSynchronous();
|
||||||
|
// public HasParam(String theTargetResourceType, String theReferenceFieldName, String theParameterName, String theParameterValue) {
|
||||||
|
map.add(PARAM_HAS, new HasParam("List", "item", "_id", listIdString));
|
||||||
|
IBundleProvider bundle = myPatientDao.search(map);
|
||||||
|
List<IBaseResource> resources = bundle.getResources(0, 2);
|
||||||
|
assertThat(resources, hasSize(2));
|
||||||
|
}
|
||||||
|
|
||||||
|
private IIdType[] createPatients(int theNumberOfPatientsToCreate) {
|
||||||
|
IIdType[] patientIds = new IIdType[theNumberOfPatientsToCreate];
|
||||||
|
for(int i=0; i < theNumberOfPatientsToCreate; i++) {
|
||||||
|
patientIds[i] = myPatientDao.create(new Patient()).getId();
|
||||||
|
}
|
||||||
|
return patientIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String createList(IIdType... thePatientIds) {
|
||||||
|
ListResource list = new ListResource();
|
||||||
|
for(IIdType patientId: thePatientIds) {
|
||||||
|
list.addEntry().getItem().setReferenceElement(patientId);
|
||||||
|
}
|
||||||
|
return myListResourceDao.create(list).getId().getIdPart();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -131,6 +131,9 @@ public class MatchUrlService {
|
||||||
paramMap.add(nextParamName, param);
|
paramMap.add(nextParamName, param);
|
||||||
} else if (JpaConstants.PARAM_DELETE_EXPUNGE.equals(nextParamName)) {
|
} else if (JpaConstants.PARAM_DELETE_EXPUNGE.equals(nextParamName)) {
|
||||||
paramMap.setDeleteExpunge(true);
|
paramMap.setDeleteExpunge(true);
|
||||||
|
} else if (Constants.PARAM_LIST.equals(nextParamName)) {
|
||||||
|
IQueryParameterAnd<?> param = ParameterUtil.parseQueryParams(myContext, RestSearchParameterTypeEnum.TOKEN, nextParamName, paramList);
|
||||||
|
paramMap.add(nextParamName, param);
|
||||||
} else if (nextParamName.startsWith("_")) {
|
} else if (nextParamName.startsWith("_")) {
|
||||||
// ignore these since they aren't search params (e.g. _sort)
|
// ignore these since they aren't search params (e.g. _sort)
|
||||||
} else {
|
} else {
|
||||||
|
|
Loading…
Reference in New Issue