diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/Constants.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/Constants.java index 8568c79f4fc..06b3d4b897d 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/Constants.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/Constants.java @@ -188,6 +188,7 @@ public class Constants { public static final String PARAM_SOURCE = "_source"; public static final String PARAM_SUMMARY = "_summary"; 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_TEXT = "_text"; public static final String PARAM_VALIDATE = "_validate"; diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/1509-_list-search-feature.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/1509-_list-search-feature.yaml new file mode 100644 index 00000000000..c84b6d87136 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/1509-_list-search-feature.yaml @@ -0,0 +1,4 @@ +--- +type: add +issue: 2417 +title: "Support for the FHIR _list search parameter." diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java index 9277d1c2c10..e140bef77d6 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java @@ -57,6 +57,7 @@ import ca.uhn.fhir.jpa.searchparam.MatchUrlService; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.jpa.util.JpaInterceptorBroadcaster; 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.rest.api.CacheControlDirective; 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.storage.ResourcePersistentId; 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.IRestfulServerDefaults; import ca.uhn.fhir.rest.server.RestfulServerUtils; @@ -120,12 +122,12 @@ import javax.annotation.PostConstruct; import javax.persistence.NoResultException; import javax.persistence.TypedQuery; import javax.servlet.http.HttpServletResponse; -import javax.validation.constraints.NotNull; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.Date; import java.util.HashSet; +import java.util.Iterator; import java.util.List; import java.util.Optional; import java.util.Set; @@ -1301,7 +1303,54 @@ public abstract class BaseHapiFhirResourceDao 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 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> andOrValues = theParams.get(key); + theParams.remove(key); + List> hasParamValues = new ArrayList<>(); + for (List orValues : andOrValues) { + List 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) { ActionRequestDetails requestDetails = new ActionRequestDetails(theRequest, getContext(), getResourceName(), null); notifyInterceptors(RestOperationTypeEnum.SEARCH_TYPE, requestDetails); @@ -1335,26 +1384,6 @@ public abstract class BaseHapiFhirResourceDao extends B 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 diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoSearchListTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoSearchListTest.java new file mode 100644 index 00000000000..093b06805e5 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoSearchListTest.java @@ -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 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 resources = bundle.getResources(0, theExpectedPatientIds.length); + assertThat(resources, hasSize(theExpectedPatientIds.length)); + + Set 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 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(); + } + + +} diff --git a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/MatchUrlService.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/MatchUrlService.java index 3827a167b9c..1927b4e9b6e 100644 --- a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/MatchUrlService.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/MatchUrlService.java @@ -131,6 +131,9 @@ public class MatchUrlService { paramMap.add(nextParamName, param); } else if (JpaConstants.PARAM_DELETE_EXPUNGE.equals(nextParamName)) { 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("_")) { // ignore these since they aren't search params (e.g. _sort) } else {