diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/repository/IRepository.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/repository/IRepository.java index 805983e1cdd8..839fe09150dd 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/repository/IRepository.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/repository/IRepository.java @@ -22,6 +22,7 @@ import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.i18n.Msg; import ca.uhn.fhir.model.api.IQueryParameterType; +import ca.uhn.fhir.repository.impl.MultiMapRepositoryRestQueryBuilder; import ca.uhn.fhir.rest.api.MethodOutcome; import ca.uhn.fhir.rest.server.exceptions.AuthenticationException; import ca.uhn.fhir.rest.server.exceptions.ForbiddenOperationException; @@ -82,6 +83,15 @@ * level of support for interactions should be. *

* + *

+ * Note to implementors: this interface exposes several search() methods. + * Several are deprecated and present for source-compatibility with previous versions + * of this interface used by the clinical reasoning project. + * We provide two main apis with different payloads for the query parameters: a Multimap method, + * and an abstract builder callback. Implementations must implement at least one of these two. + * We provide default implementations of each in terms of the other. + *

+ * * @see FHIR REST API */ @Beta @@ -262,7 +272,9 @@ default B search( * @param resourceType the class of the Resource type to search * @param searchParameters the searchParameters for this search * @return a Bundle with the results of the search + * @deprecated since 8.4 use Multimap instead */ + @Deprecated(since = "8.4.0") default B search( Class bundleType, Class resourceType, Map> searchParameters) { return this.search(bundleType, resourceType, searchParameters, Collections.emptyMap()); @@ -275,18 +287,48 @@ default B search( * * @param a Bundle type * @param a Resource type - * @param bundleType the class of the Bundle type to return - * @param resourceType the class of the Resource type to search - * @param searchParameters the searchParameters for this search - * @param headers headers for this request, typically key-value pairs of HTTP headers + * @param theBundleType the class of the Bundle type to return + * @param theResourceType the class of the Resource type to search + * @param theSearchParameters the searchParameters for this search + * @param theHeaders headers for this request, typically key-value pairs of HTTP headers * @return a Bundle with the results of the search */ - B search( - Class bundleType, - Class resourceType, - Multimap> searchParameters, - Map headers); + default B search( + Class theBundleType, + Class theResourceType, + Multimap> theSearchParameters, + Map theHeaders) { + // we have a cycle of default implementations between this and the search builder version. + // Implementors MUST implement one or the other or both. + return this.search(theBundleType, theResourceType, sb -> sb.addAll(theSearchParameters), theHeaders); + } + /** + * Searches this repository + * + * @see FHIR search + * + * @param a Bundle type + * @param a Resource type + * @param theBundleType the class of the Bundle type to return + * @param theResourceType the class of the Resource type to search + * @param theQueryContributor the searchParameters for this search + * @param theHeaders headers for this request, typically key-value pairs of HTTP headers + * @return a Bundle with the results of the search + */ + default B search( + Class theBundleType, + Class theResourceType, + IRepositoryRestQueryContributor theQueryContributor, + Map theHeaders) { + // we have a cycle of default implementations between this and the multi-map version. + // Implementors MUST implement one or the other for now. + return this.search( + theBundleType, + theResourceType, + MultiMapRepositoryRestQueryBuilder.contributorToMultimap(theQueryContributor), + theHeaders); + } /** * Searches this repository * @@ -299,7 +341,9 @@ B search( * @param searchParameters the searchParameters for this search * @param headers headers for this request, typically key-value pairs of HTTP headers * @return a Bundle with the results of the search + * @deprecated since 8.4 use Multimap instead */ + @Deprecated(since = "8.4.0") default B search( Class bundleType, Class resourceType, @@ -737,4 +781,12 @@ default B private static T throwNotImplementedOperationException(String theMessage) { throw new NotImplementedOperationException(Msg.code(2542) + theMessage); } + + /** + * Callback interface for search() methods that use a builder to construct the query. + */ + @FunctionalInterface + interface IRepositoryRestQueryContributor { + void contributeToQuery(IRepositoryRestQueryBuilder theBuilder); + } } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/repository/IRepositoryRestQueryBuilder.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/repository/IRepositoryRestQueryBuilder.java new file mode 100644 index 000000000000..2fbd228da591 --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/repository/IRepositoryRestQueryBuilder.java @@ -0,0 +1,41 @@ +package ca.uhn.fhir.repository; + +import ca.uhn.fhir.model.api.IQueryParameterType; +import ca.uhn.fhir.rest.param.NumberParam; +import com.google.common.collect.Multimap; + +import java.util.List; +import java.util.Map; + +/** + * Abstract interface for building a repository rest query. + */ +public interface IRepositoryRestQueryBuilder { + + /** + * The main method for implementations to add a parameter to the query. + * @param theParamName the search parameter name, without modifiers. E.g. "name", or "_sort" + * @param theParameters a list of parameters - this is the comma-separated list after the "=" in a rest query. + * @return this for chaining + */ + IRepositoryRestQueryBuilder addOrList(String theParamName, List theParameters); + + default IRepositoryRestQueryBuilder addOrList(String theParamName, IQueryParameterType... theParameterValues) { + return addOrList(theParamName, List.of(theParameterValues)); + } + + default IRepositoryRestQueryBuilder addNumericParameter(String theParamName, int theValue) { + return addOrList(theParamName, new NumberParam(theValue)); + } + + default IRepositoryRestQueryBuilder addAll(Multimap> theSearchParameters) { + theSearchParameters.entries().forEach(e -> this.addOrList(e.getKey(), e.getValue())); + return this; + } + + default IRepositoryRestQueryBuilder addAll(Map> theSearchParameters) { + theSearchParameters.forEach(this::addOrList); + return this; + } + +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/repository/impl/MultiMapRepositoryRestQueryBuilder.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/repository/impl/MultiMapRepositoryRestQueryBuilder.java new file mode 100644 index 000000000000..6b0d76ec9194 --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/repository/impl/MultiMapRepositoryRestQueryBuilder.java @@ -0,0 +1,61 @@ +package ca.uhn.fhir.repository.impl; + +import ca.uhn.fhir.model.api.IQueryParameterType; +import ca.uhn.fhir.repository.IRepository; +import ca.uhn.fhir.repository.IRepositoryRestQueryBuilder; +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.Multimap; + +import java.util.List; + +/** + * This class provides a rest-query builder over a plain Multimap. + * It is used to convert {@link IRepository.IRepositoryRestQueryContributor} implementations + * that are not Multimap-based so they can be used by IRepository implementations that are. + */ +public class MultiMapRepositoryRestQueryBuilder implements IRepositoryRestQueryBuilder { + /** + * Our search parameters. + * We use a list multimap to maintain insertion order, and because most of IQueryParameterType don't + * provide a meaningful equals/hashCode implementation. + */ + private final Multimap> mySearchParameters = ArrayListMultimap.create(); + + @Override + public IRepositoryRestQueryBuilder addOrList(String theParamName, List theParameters) { + validateHomogeneousList(theParamName, theParameters); + mySearchParameters.put(theParamName, theParameters); + return this; + } + + private void validateHomogeneousList(String theName, List theValues) { + if (theValues.isEmpty()) { + return; + } + IQueryParameterType firstValue = theValues.get(0); + for (IQueryParameterType nextValue : theValues) { + if (!nextValue.getClass().equals(firstValue.getClass())) { + throw new IllegalArgumentException("All parameters in a or-list must be of the same type. Found " + + firstValue.getClass().getSimpleName() + " and " + + nextValue.getClass().getSimpleName() + " in parameter '" + theName + "'"); + } + } + } + + public Multimap> toMultiMap() { + return mySearchParameters; + } + + /** + * Converts a {@link IRepository.IRepositoryRestQueryContributor} to a Multimap. + * + * @param theSearchQueryBuilder the contributor to convert + * @return a Multimap containing the search parameters contributed by the contributor + */ + public static Multimap> contributorToMultimap( + IRepository.IRepositoryRestQueryContributor theSearchQueryBuilder) { + MultiMapRepositoryRestQueryBuilder builder = new MultiMapRepositoryRestQueryBuilder(); + theSearchQueryBuilder.contributeToQuery(builder); + return builder.toMultiMap(); + } +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/SortSpec.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/SortSpec.java index 9c97c36fe0e9..804423687183 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/SortSpec.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/SortSpec.java @@ -20,7 +20,11 @@ package ca.uhn.fhir.rest.api; import ca.uhn.fhir.i18n.Msg; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; +import org.apache.commons.lang3.builder.ToStringBuilder; +import java.io.Serial; import java.io.Serializable; /** @@ -29,6 +33,7 @@ */ public class SortSpec implements Serializable { + @Serial private static final long serialVersionUID = 2866833099879713467L; private SortSpec myChain; @@ -138,4 +143,52 @@ public SortSpec setOrder(SortOrderEnum theOrder) { myOrder = theOrder; return this; } + + @Override + public boolean equals(Object theO) { + if (this == theO) return true; + + if (!(theO instanceof SortSpec sortSpec)) return false; + + return new EqualsBuilder() + .append(myChain, sortSpec.myChain) + .append(myParamName, sortSpec.myParamName) + .append(myOrder, sortSpec.myOrder) + .isEquals(); + } + + @Override + public int hashCode() { + return new HashCodeBuilder(17, 37) + .append(myChain) + .append(myParamName) + .append(myOrder) + .toHashCode(); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .append("myParamName", myParamName) + .append("myOrder", myOrder) + .append("myChain", myChain) + .toString(); + } + + /** + * Convert strings like "-date" into a SortSpec object. + * Note: this does not account for DSTU2-style sort modifiers like "date:desc" or "date:asc" + * since those are on the parameter name, not the value. + * + * @param theParamValue a string like "-date" or "date" + * @return a parsed SortSpec object + */ + public static SortSpec fromR3OrLaterParameterValue(String theParamValue) { + SortOrderEnum direction = SortOrderEnum.ASC; + if (theParamValue.startsWith("-")) { + direction = SortOrderEnum.DESC; + theParamValue = theParamValue.substring(1); + } + return new SortSpec(theParamValue, direction); + } } diff --git a/hapi-fhir-base/src/test/java/ca/uhn/fhir/repository/impl/MultiMapRepositoryRestQueryBuilderTest.java b/hapi-fhir-base/src/test/java/ca/uhn/fhir/repository/impl/MultiMapRepositoryRestQueryBuilderTest.java new file mode 100644 index 000000000000..8bafd883156a --- /dev/null +++ b/hapi-fhir-base/src/test/java/ca/uhn/fhir/repository/impl/MultiMapRepositoryRestQueryBuilderTest.java @@ -0,0 +1,52 @@ +package ca.uhn.fhir.repository.impl; + +import ca.uhn.fhir.model.api.IQueryParameterType; +import ca.uhn.fhir.rest.param.TokenParam; +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.LinkedHashMultimap; +import com.google.common.collect.Multimap; +import com.google.common.collect.Multimaps; +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class MultiMapRepositoryRestQueryBuilderTest { + MultiMapRepositoryRestQueryBuilder myBuilder = new MultiMapRepositoryRestQueryBuilder(); + + @Test + void testAddAllMultimap() { + // given + Multimap> map = ArrayListMultimap.create(); + map.put("param1", List.of(new TokenParam("value1"), new TokenParam("value2"))); + map.put("param1", List.of(new TokenParam("value3"))); + map.put("param2", List.of(new TokenParam("value4"))); + + // when + myBuilder.addAll(map); + + // then + assertEquals(map, myBuilder.toMultiMap()); + } + + + @Test + void testAddAllMap() { + // given + Map> map = new HashMap<>(); + map.put("param1", List.of(new TokenParam("value1"), new TokenParam("value2"))); + map.put("param2", List.of(new TokenParam("value4"))); + + // when + myBuilder.addAll(map); + + // then + // use a set for comparison. + var actualAsSetMultiMap = LinkedHashMultimap.create(myBuilder.toMultiMap()); + assertEquals(Multimaps.forMap(map), actualAsSetMultiMap); + } + +} diff --git a/hapi-fhir-base/src/test/java/ca/uhn/fhir/repository/impl/MultiMapSearchQueryBuilderTest.java b/hapi-fhir-base/src/test/java/ca/uhn/fhir/repository/impl/MultiMapSearchQueryBuilderTest.java new file mode 100644 index 000000000000..1bc2dda0073c --- /dev/null +++ b/hapi-fhir-base/src/test/java/ca/uhn/fhir/repository/impl/MultiMapSearchQueryBuilderTest.java @@ -0,0 +1,80 @@ +package ca.uhn.fhir.repository.impl; + +import ca.uhn.fhir.model.api.IQueryParameterType; +import ca.uhn.fhir.rest.param.StringParam; +import ca.uhn.fhir.rest.param.TokenParam; +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.Multimap; +import org.junit.jupiter.api.Test; + +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class MultiMapSearchQueryBuilderTest { + MultiMapRepositoryRestQueryBuilder myMultiMapSearchQueryBuilder = new MultiMapRepositoryRestQueryBuilder(); + + @Test + void testAddAllMultiMap() { + // given + Multimap> map = ArrayListMultimap.create(); + map.put("key1", List.of(new StringParam("value1"))); + + + // when + myMultiMapSearchQueryBuilder.addAll(map); + + // then + assertEquals(map, myMultiMapSearchQueryBuilder.toMultiMap()); + } + + + @Test + void testAddAllMap() { + // given + Map> map = new HashMap<>(); + map.put("key1", List.of(new StringParam("value1"))); + + + // when + myMultiMapSearchQueryBuilder.addAll(map); + + // then + Multimap> multiMap = myMultiMapSearchQueryBuilder.toMultiMap(); + assertThat(multiMap.size()).isEqualTo(1); + Collection> values = multiMap.get("key1"); + assertThat(values).hasSize(1); + assertThat(values.iterator().next()).isEqualTo(List.of(new StringParam("value1"))); + } + + @Test + void testAddOrList() { + // given + + // when + myMultiMapSearchQueryBuilder.addOrList("code", new TokenParam("system", "code1"), new TokenParam("system", "code2")); + myMultiMapSearchQueryBuilder.addOrList("code", new TokenParam("system", "code3"), new TokenParam("system", "code4")); + + // then + Multimap> multiMap = myMultiMapSearchQueryBuilder.toMultiMap(); + + Multimap> expected = ArrayListMultimap.create(); + expected.put("code", List.of(new TokenParam("system", "code1"), new TokenParam("system", "code2"))); + expected.put("code", List.of(new TokenParam("system", "code3"), new TokenParam("system", "code4"))); + + assertThat(multiMap).isEqualTo(expected); + } + + @Test + void testOrListWithMixedTypesThrowsException() { + IQueryParameterType[] mixedParams = {new TokenParam("system", "code1"), new StringParam("string value")}; + assertThrows(IllegalArgumentException.class, () -> myMultiMapSearchQueryBuilder.addOrList("code", mixedParams)); + } + + +} diff --git a/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/api/SortSpecTest.java b/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/api/SortSpecTest.java new file mode 100644 index 000000000000..43093f93eddf --- /dev/null +++ b/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/api/SortSpecTest.java @@ -0,0 +1,52 @@ +package ca.uhn.fhir.rest.api; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +class SortSpecTest { + + @Test + void testSortSpecCreation() { + SortSpec sortSpec = new SortSpec("name", SortOrderEnum.ASC); + assertEquals("name", sortSpec.getParamName()); + assertEquals(SortOrderEnum.ASC, sortSpec.getOrder()); + } + + @Test + void testSortSpecWithChain() { + SortSpec chain = new SortSpec("date", SortOrderEnum.DESC); + SortSpec sortSpec = new SortSpec("name", SortOrderEnum.ASC, chain); + assertEquals("name", sortSpec.getParamName()); + assertEquals(SortOrderEnum.ASC, sortSpec.getOrder()); + assertNotNull(sortSpec.getChain()); + assertEquals("date", sortSpec.getChain().getParamName()); + assertEquals(SortOrderEnum.DESC, sortSpec.getChain().getOrder()); + } + + @Test + void testParse() { + // given + String value = "date"; + + // when + SortSpec sortSpec = SortSpec.fromR3OrLaterParameterValue(value); + + // then + assertEquals(new SortSpec("date", SortOrderEnum.ASC), sortSpec); + } + + @Test + void testParseDesc() { + // given + String value = "-date"; + + // when + SortSpec sortSpec = SortSpec.fromR3OrLaterParameterValue(value); + + // then + assertEquals(new SortSpec("date", SortOrderEnum.DESC), sortSpec); + } + +} 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 8ade6ce89e2f..295ff178d2da 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 @@ -70,6 +70,11 @@ public MatchUrlService() { super(); } + public MatchUrlService(FhirContext theFhirContext, ISearchParamRegistry theSearchParamRegistry) { + myFhirContext = theFhirContext; + mySearchParamRegistry = theSearchParamRegistry; + } + public SearchParameterMap translateMatchUrl( String theMatchUrl, RuntimeResourceDefinition theResourceDefinition, Flag... theFlags) { SearchParameterMap paramMap = new SearchParameterMap(); diff --git a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/SearchParameterMap.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/SearchParameterMap.java index d6bf7ce0adf6..a48fb6355138 100644 --- a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/SearchParameterMap.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/SearchParameterMap.java @@ -24,6 +24,8 @@ import ca.uhn.fhir.model.api.IQueryParameterOr; import ca.uhn.fhir.model.api.IQueryParameterType; import ca.uhn.fhir.model.api.Include; +import ca.uhn.fhir.repository.IRepository; +import ca.uhn.fhir.repository.IRepositoryRestQueryBuilder; import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.api.SearchContainedModeEnum; import ca.uhn.fhir.rest.api.SearchIncludeDeletedEnum; @@ -59,6 +61,8 @@ import java.util.Set; import java.util.stream.Collectors; +import static ca.uhn.fhir.rest.api.Constants.PARAM_COUNT; +import static ca.uhn.fhir.rest.api.Constants.PARAM_INCLUDE; import static ca.uhn.fhir.rest.param.ParamPrefixEnum.GREATERTHAN_OR_EQUALS; import static ca.uhn.fhir.rest.param.ParamPrefixEnum.LESSTHAN_OR_EQUALS; import static ca.uhn.fhir.rest.param.ParamPrefixEnum.NOT_EQUAL; @@ -66,7 +70,7 @@ import static org.apache.commons.lang3.StringUtils.isBlank; import static org.apache.commons.lang3.StringUtils.isNotBlank; -public class SearchParameterMap implements Serializable { +public class SearchParameterMap implements Serializable, IRepository.IRepositoryRestQueryContributor { public static final Integer INTEGER_0 = 0; private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SearchParameterMap.class); private static final long serialVersionUID = 1L; @@ -167,11 +171,9 @@ public SearchParameterMap add(String theName, IQueryParameterAnd theAnd) { if (theAnd == null) { return this; } - if (!containsKey(theName)) { - put(theName, new ArrayList<>()); - } - List> paramList = get(theName); + List> paramList = getOrCreate(theName); + for (IQueryParameterOr next : theAnd.getValuesAsQueryTokens()) { if (next == null) { continue; @@ -182,15 +184,25 @@ public SearchParameterMap add(String theName, IQueryParameterAnd theAnd) { return this; } + private List> getOrCreate(String theName) { + return mySearchParameterMap.computeIfAbsent(theName, k -> new ArrayList<>()); + } + public SearchParameterMap add(String theName, IQueryParameterOr theOr) { if (theOr == null) { return this; } - if (!containsKey(theName)) { - put(theName, new ArrayList<>()); + return addOrList(theName, (List) theOr.getValuesAsQueryTokens()); + } + + @Nonnull + public SearchParameterMap addOrList(String theName, @Nonnull List theOrValues) { + if (theOrValues.isEmpty()) { + return this; } - get(theName).add((List) theOr.getValuesAsQueryTokens()); + getOrCreate(theName).add(theOrValues); + return this; } @@ -204,12 +216,9 @@ public SearchParameterMap add(String theName, IQueryParameterType theParam) { if (theParam == null) { return this; } - if (!containsKey(theName)) { - put(theName, new ArrayList<>()); - } ArrayList list = new ArrayList<>(); list.add(theParam); - get(theName).add(list); + getOrCreate(theName).add(list); return this; } @@ -505,7 +514,7 @@ public String toNormalizedQueryString(FhirContext theCtx) { } if (hasIncludes()) { - addUrlIncludeParams(b, Constants.PARAM_INCLUDE, getIncludes()); + addUrlIncludeParams(b, PARAM_INCLUDE, getIncludes()); } addUrlIncludeParams(b, Constants.PARAM_REVINCLUDE, getRevIncludes()); @@ -523,7 +532,7 @@ public String toNormalizedQueryString(FhirContext theCtx) { if (getCount() != null) { addUrlParamSeparator(b); - b.append(Constants.PARAM_COUNT); + b.append(PARAM_COUNT); b.append('='); b.append(getCount()); } @@ -575,6 +584,17 @@ public String toNormalizedQueryString(FhirContext theCtx) { return b.toString(); } + /** + * Configure a query with the current settings. + * This is an adaptor method to allow this class to be used in IRepository.search(). + * with repository implementations that don't use SearchParameterMap. + * @param theBuilder the builder to configure with our settings + */ + @Override + public void contributeToQuery(IRepositoryRestQueryBuilder theBuilder) { + new SearchParameterMapContributor(this, theBuilder).contributeToQuery(); + } + private boolean isNotEqualsComparator(DateParam theLowerBound, DateParam theUpperBound) { return theLowerBound != null && theUpperBound != null diff --git a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/SearchParameterMapContributor.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/SearchParameterMapContributor.java new file mode 100644 index 000000000000..192a715c88cc --- /dev/null +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/SearchParameterMapContributor.java @@ -0,0 +1,96 @@ +package ca.uhn.fhir.jpa.searchparam; + +import ca.uhn.fhir.model.api.IQueryParameterType; +import ca.uhn.fhir.model.api.Include; +import ca.uhn.fhir.repository.IRepositoryRestQueryBuilder; +import ca.uhn.fhir.rest.api.SearchContainedModeEnum; +import ca.uhn.fhir.rest.api.SearchTotalModeEnum; +import ca.uhn.fhir.rest.api.SortOrderEnum; +import ca.uhn.fhir.rest.api.SortSpec; +import ca.uhn.fhir.rest.api.SummaryEnum; +import ca.uhn.fhir.rest.param.TokenParam; +import jakarta.annotation.Nonnull; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.function.Function; + +import static ca.uhn.fhir.rest.api.Constants.PARAM_CONTAINED; +import static ca.uhn.fhir.rest.api.Constants.PARAM_COUNT; +import static ca.uhn.fhir.rest.api.Constants.PARAM_INCLUDE; +import static ca.uhn.fhir.rest.api.Constants.PARAM_INCLUDE_ITERATE; +import static ca.uhn.fhir.rest.api.Constants.PARAM_OFFSET; +import static ca.uhn.fhir.rest.api.Constants.PARAM_REVINCLUDE; +import static ca.uhn.fhir.rest.api.Constants.PARAM_REVINCLUDE_ITERATE; +import static ca.uhn.fhir.rest.api.Constants.PARAM_SEARCH_TOTAL_MODE; +import static ca.uhn.fhir.rest.api.Constants.PARAM_SORT; +import static ca.uhn.fhir.rest.api.Constants.PARAM_SUMMARY; + +record SearchParameterMapContributor(SearchParameterMap mySearchParameterMap, IRepositoryRestQueryBuilder myBuilder) { + + public void contributeToQuery() { + addSearchParameters(); + addToken(PARAM_CONTAINED, mySearchParameterMap.getSearchContainedMode(), SearchContainedModeEnum::getCode); + addNumeric(PARAM_COUNT, mySearchParameterMap.getCount()); + addNumeric(PARAM_OFFSET, mySearchParameterMap.getOffset()); + addSort(mySearchParameterMap.getSort()); + addToken(PARAM_SUMMARY, mySearchParameterMap.getSummaryMode(), SummaryEnum::getCode); + addToken(PARAM_SEARCH_TOTAL_MODE, mySearchParameterMap.getSearchTotalMode(), SearchTotalModeEnum::getCode); + addIncludes(PARAM_INCLUDE, PARAM_INCLUDE_ITERATE, mySearchParameterMap.getIncludes()); + addIncludes(PARAM_REVINCLUDE, PARAM_REVINCLUDE_ITERATE, mySearchParameterMap.getRevIncludes()); + } + + private void addSearchParameters() { + mySearchParameterMap.entrySet().forEach(nextAndOrListEntry -> { + for (List nextOrList : nextAndOrListEntry.getValue()) { + if (nextOrList.isEmpty()) { + continue; + } + myBuilder.addOrList(nextAndOrListEntry.getKey(), nextOrList); + } + }); + } + + private void addIncludes(String paramInclude, String paramIncludeIterate, Set theIncludes) { + for (Include nextInclude : theIncludes) { + if (nextInclude.isRecurse()) { + myBuilder.addOrList(paramIncludeIterate, new TokenParam(nextInclude.getValue())); + } else { + myBuilder.addOrList(paramInclude, new TokenParam(nextInclude.getValue())); + } + } + } + + private void addNumeric(String theParamName, Integer theValue) { + if (theValue != null) { + myBuilder.addNumericParameter(theParamName, theValue); + } + } + + private void addSort(SortSpec theSort) { + SortSpec sort = theSort; + if (sort != null) { + List paraValues = new ArrayList<>(); + for (; sort != null; sort = sort.getChain()) { + paraValues.add(new TokenParam(toSortString(sort))); + } + myBuilder.addOrList(PARAM_SORT, paraValues); + } + } + + @Nonnull + private static String toSortString(SortSpec sort) { + if (SortOrderEnum.DESC.equals(sort.getOrder())) { + return "-" + sort.getParamName(); + } else { + return sort.getParamName(); + } + } + + private void addToken(String theParamName, T theValue, Function theConverter) { + if (theValue != null) { + myBuilder.addOrList(theParamName, new TokenParam(theConverter.apply(theValue))); + } + } +} diff --git a/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/SearchParameterMapTest.java b/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/SearchParameterMapTest.java index e221a0241051..fcd2c7eba284 100644 --- a/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/SearchParameterMapTest.java +++ b/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/SearchParameterMapTest.java @@ -6,19 +6,28 @@ import ca.uhn.fhir.rest.api.SearchContainedModeEnum; import ca.uhn.fhir.rest.api.SearchTotalModeEnum; import ca.uhn.fhir.rest.api.SortSpec; +import ca.uhn.fhir.rest.api.SummaryEnum; import ca.uhn.fhir.rest.param.DateRangeParam; +import ca.uhn.fhir.rest.param.NumberParam; import ca.uhn.fhir.rest.param.QuantityParam; import ca.uhn.fhir.rest.param.StringParam; import ca.uhn.fhir.rest.param.TokenOrListParam; import ca.uhn.fhir.rest.param.TokenParam; +import com.google.common.collect.Multimap; +import com.google.common.collect.MultimapBuilder; +import com.google.common.collect.Multimaps; +import com.google.common.collect.SetMultimap; +import jakarta.annotation.Nonnull; import org.junit.jupiter.api.Test; import java.sql.Date; import java.time.Instant; import java.util.HashSet; import java.util.List; +import java.util.Map; import static ca.uhn.fhir.jpa.searchparam.SearchParameterMap.compare; +import static ca.uhn.fhir.repository.impl.MultiMapRepositoryRestQueryBuilder.contributorToMultimap; import static ca.uhn.fhir.rest.param.TokenParamModifier.TEXT; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -221,6 +230,44 @@ public void testCompareParameters() { } + @Test + void testConversionToMultiMap() { + // given + SearchParameterMap map = new SearchParameterMap(); + map.add("name", new StringParam("John")); + map.setSearchContainedMode(SearchContainedModeEnum.FALSE); + map.setCount(5); + map.setOffset(52); + map.setSort(new SortSpec("name")); + map.setSummaryMode(SummaryEnum.COUNT); + map.setSearchTotalMode(SearchTotalModeEnum.ACCURATE); + map.addInclude(new Include("Patient:general-practitioner")); + map.addInclude(new Include("Patient:organization", true)); + map.addRevInclude(new Include("Observation:subject")); + + // when + Multimap> multimap = contributorToMultimap(map); + + // then + Map> expected = Map.of( + "name", List.of(new StringParam("John")), + "_contained", List.of(new TokenParam("false")), + "_count", List.of(new NumberParam(5)), + "_offset", List.of(new NumberParam(52)), + "_sort", List.of(new TokenParam("name")), + "_summary", List.of(new TokenParam("count")), + "_total", List.of(new TokenParam("accurate")), + "_include", List.of(new TokenParam("Patient:general-practitioner")), + "_include:iterate", List.of(new TokenParam("Patient:organization")), + "_revinclude", List.of(new TokenParam("Observation:subject")) + ); + assertEquals(asSetMultimap(Multimaps.forMap(expected)).toString(), asSetMultimap(multimap).toString()); + } - + @Nonnull + private static SetMultimap< String, List> asSetMultimap(Multimap> multimap) { + SetMultimap> result = MultimapBuilder.treeKeys().hashSetValues().build(); + result.putAll(multimap); + return result; + } } diff --git a/hapi-fhir-jpaserver-test-utilities/src/test/java/ca/uhn/fhir/jpa/dao/HapiFhirRepositoryTest.java b/hapi-fhir-jpaserver-test-utilities/src/test/java/ca/uhn/fhir/jpa/dao/HapiFhirRepositoryTest.java index 369127ff67b2..aa4b6323539f 100644 --- a/hapi-fhir-jpaserver-test-utilities/src/test/java/ca/uhn/fhir/jpa/dao/HapiFhirRepositoryTest.java +++ b/hapi-fhir-jpaserver-test-utilities/src/test/java/ca/uhn/fhir/jpa/dao/HapiFhirRepositoryTest.java @@ -1,14 +1,58 @@ package ca.uhn.fhir.jpa.dao; +import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.jpa.api.config.JpaStorageSettings; import ca.uhn.fhir.jpa.model.entity.NormalizedQuantitySearchLevel; import ca.uhn.fhir.jpa.repository.HapiFhirRepository; +import ca.uhn.fhir.jpa.rp.r4.PatientResourceProvider; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.jpa.test.BaseJpaR4Test; import ca.uhn.fhir.repository.IRepositoryTest; +import ca.uhn.fhir.rest.api.server.SystemRequestDetails; +import ca.uhn.fhir.rest.param.ReferenceParam; +import ca.uhn.fhir.rest.server.RestfulServer; +import ca.uhn.fhir.util.BundleBuilder; +import ca.uhn.fhir.util.BundleUtil; +import ca.uhn.fhir.util.bundle.SearchBundleEntryParts; +import jakarta.servlet.ServletException; +import org.hl7.fhir.instance.model.api.IBaseBundle; +import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.r4.model.Meta; +import org.hl7.fhir.r4.model.Parameters; import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; class HapiFhirRepositoryTest extends BaseJpaR4Test implements IRepositoryTest { + private HapiFhirRepository myRepository; + + @BeforeEach + public void beforeResetDao() { + + RestfulServer restfulServer = new RestfulServer(myFhirContext); + + PatientResourceProvider patientResourceProvider = new PatientResourceProvider(); + patientResourceProvider.setDao(myPatientDao); + patientResourceProvider.setContext(myFhirContext); + + restfulServer.setResourceProviders(patientResourceProvider); + try { + restfulServer.init(); + } catch (ServletException e) { + throw new RuntimeException(e); + } + var srd = new SystemRequestDetails(restfulServer.getInterceptorService()); + srd.setFhirContext(myFhirContext); + srd.setServer(restfulServer); + myRepository = new HapiFhirRepository(myDaoRegistry, srd, restfulServer); + } + @AfterEach public void afterResetDao() { myStorageSettings.setResourceServerIdStrategy(new JpaStorageSettings().getResourceServerIdStrategy()); @@ -22,11 +66,43 @@ public void afterResetDao() { @Override public RepositoryTestSupport getRepositoryTestSupport() { - return new RepositoryTestSupport(new HapiFhirRepository(myDaoRegistry, mySrd, null)); + return new RepositoryTestSupport(myRepository); } - @Override - public boolean isSearchSupported() { - return false; + @Test + void testSearchBySearchParameterMap() { + // given + FhirContext context = getRepository().fhirContext(); + var repository = getRepository(); + var b = getTestDataBuilder(); + var patientClass = getTestDataBuilder().buildPatient().getClass(); + b.createPatient(b.withId("abc")); + b.createPatient(b.withId("def")); + IBaseBundle bundle = new BundleBuilder(context).getBundle(); + + SearchParameterMap searchParams = new SearchParameterMap(); + searchParams.addOrList("_id", List.of(new ReferenceParam("abc"), new ReferenceParam("ghi"))); + + // when + IBaseBundle searchResult = repository.search(bundle.getClass(), patientClass, searchParams, Map.of()); + + // then + List entries = BundleUtil.getSearchBundleEntryParts(context, searchResult); + assertThat(entries).hasSize(1); + SearchBundleEntryParts entry = entries.get(0); + assertThat(entry.getResource()).isNotNull(); + assertThat(entry.getResource()).isInstanceOf(patientClass); + } + + @Test + void testOperation() { + IIdType patientId = getTestDataBuilder().createPatient(withId("abc")); + + Parameters result = getRepository().invoke(patientId, "$meta", null, Parameters.class, Map.of()); + + assertThat(result).isNotNull(); + Meta meta = (Meta) result.getParameter("return").getValue(); + assertThat(meta.getVersionId()).isEqualTo("1"); } + } diff --git a/hapi-fhir-storage/pom.xml b/hapi-fhir-storage/pom.xml index 694a433796d2..0db409bb82a1 100644 --- a/hapi-fhir-storage/pom.xml +++ b/hapi-fhir-storage/pom.xml @@ -30,6 +30,11 @@ + + ca.uhn.hapi.fhir + hapi-fhir-repositories + ${project.version} + ca.uhn.hapi.fhir hapi-fhir-server diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/repository/HapiFhirRepository.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/repository/HapiFhirRepository.java index a4837e2757a1..ff7ddd853c6c 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/repository/HapiFhirRepository.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/repository/HapiFhirRepository.java @@ -23,7 +23,8 @@ import ca.uhn.fhir.i18n.Msg; import ca.uhn.fhir.jpa.api.dao.DaoRegistry; import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome; -import ca.uhn.fhir.model.api.IQueryParameterType; +import ca.uhn.fhir.jpa.repository.searchparam.SearchParameterMapRepositoryRestQueryBuilder; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.model.api.Include; import ca.uhn.fhir.model.valueset.BundleTypeEnum; import ca.uhn.fhir.repository.IRepository; @@ -43,7 +44,6 @@ import ca.uhn.fhir.rest.server.method.ConformanceMethodBinding; import ca.uhn.fhir.rest.server.method.PageMethodBinding; import ca.uhn.fhir.util.UrlUtil; -import com.google.common.collect.Multimap; import jakarta.annotation.Nonnull; import org.hl7.fhir.instance.model.api.IBaseBundle; import org.hl7.fhir.instance.model.api.IBaseConformance; @@ -53,7 +53,6 @@ import java.io.IOException; import java.util.HashSet; -import java.util.List; import java.util.Map; import java.util.Set; @@ -61,7 +60,8 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank; /** - * This class leverages DaoRegistry from Hapi-fhir to implement CRUD FHIR API operations constrained to provide only the operations necessary for the cql-evaluator modules to function. + * This class leverages DaoRegistry from Hapi-fhir JPA server to implement IRepository. + * This does not implement history. **/ @SuppressWarnings("squid:S1135") public class HapiFhirRepository implements IRepository { @@ -141,18 +141,19 @@ public MethodOutcome delete( public B search( Class theBundleType, Class theResourceType, - Multimap> theSearchParameters, + IRepositoryRestQueryContributor theQueryContributor, Map theHeaders) { RequestDetails details = startWith(myRequestDetails) .setAction(RestOperationTypeEnum.SEARCH_TYPE) .addHeaders(theHeaders) .create(); - SearchConverter converter = new SearchConverter(); - converter.convertParameters(theSearchParameters, fhirContext()); - details.setParameters(converter.myResultParameters); + SearchParameterMap searchParameterMap = + SearchParameterMapRepositoryRestQueryBuilder.buildFromQueryContributor(theQueryContributor); + // fixme need a SPMap -> Map converter + // details.setParameters(fixme); details.setResourceName(myDaoRegistry.getFhirContext().getResourceType(theResourceType)); IBundleProvider bundleProvider = - myDaoRegistry.getResourceDao(theResourceType).search(converter.mySearchParameterMap, details); + myDaoRegistry.getResourceDao(theResourceType).search(searchParameterMap, details); if (bundleProvider == null) { return null; diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/repository/RequestDetailsCloner.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/repository/RequestDetailsCloner.java index c3dde2d1908f..be67d1ea3831 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/repository/RequestDetailsCloner.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/repository/RequestDetailsCloner.java @@ -22,8 +22,10 @@ import ca.uhn.fhir.parser.IParser; import ca.uhn.fhir.rest.api.RequestTypeEnum; import ca.uhn.fhir.rest.api.RestOperationTypeEnum; +import ca.uhn.fhir.rest.api.server.IRestfulResponse; import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.api.server.SystemRequestDetails; +import ca.uhn.fhir.rest.api.server.SystemRestfulResponse; import org.hl7.fhir.instance.model.api.IBaseParameters; import org.hl7.fhir.instance.model.api.IIdType; @@ -47,7 +49,11 @@ static DetailsBuilder startWith(RequestDetails theDetails) { newDetails.setParameters(new HashMap<>()); newDetails.setResourceName(null); newDetails.setCompartmentName(null); - newDetails.setResponse(theDetails.getResponse()); + IRestfulResponse response = theDetails.getResponse(); + if (response == null) { + response = new SystemRestfulResponse(newDetails); + } + newDetails.setResponse(response); return new DetailsBuilder(newDetails); } @@ -76,8 +82,12 @@ DetailsBuilder addHeaders(Map theHeaders) { DetailsBuilder setParameters(IBaseParameters theParameters) { IParser parser = myDetails.getServer().getFhirContext().newJsonParser(); - myDetails.setRequestContents( - parser.encodeResourceToString(theParameters).getBytes()); + if (theParameters != null) { + myDetails.setRequestContents( + parser.encodeResourceToString(theParameters).getBytes()); + } else { + myDetails.setRequestContents(new byte[0]); + } return this; } diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/repository/SearchConverter.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/repository/SearchConverter.java deleted file mode 100644 index 84c68097299b..000000000000 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/repository/SearchConverter.java +++ /dev/null @@ -1,145 +0,0 @@ -/*- - * #%L - * HAPI FHIR Storage api - * %% - * Copyright (C) 2014 - 2025 Smile CDR, Inc. - * %% - * 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% - */ -package ca.uhn.fhir.jpa.repository; - -import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; -import ca.uhn.fhir.model.api.IQueryParameterAnd; -import ca.uhn.fhir.model.api.IQueryParameterOr; -import ca.uhn.fhir.model.api.IQueryParameterType; -import ca.uhn.fhir.rest.param.TokenOrListParam; -import ca.uhn.fhir.rest.param.TokenParam; -import com.google.common.collect.ArrayListMultimap; -import com.google.common.collect.Multimap; -import jakarta.annotation.Nonnull; - -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -/** - * The IGenericClient API represents searches with OrLists, while the FhirRepository API uses nested - * lists. This class (will eventually) convert between them - */ -public class SearchConverter { - // hardcoded list from FHIR specs: https://www.hl7.org/fhir/search.html - private final List mySearchResultParameters = Arrays.asList( - "_sort", - "_count", - "_include", - "_revinclude", - "_summary", - "_total", - "_elements", - "_contained", - "_containedType"); - public final Multimap> mySeparatedSearchParameters = ArrayListMultimap.create(); - public final Multimap> mySeparatedResultParameters = ArrayListMultimap.create(); - public final SearchParameterMap mySearchParameterMap = new SearchParameterMap(); - public final Map myResultParameters = new HashMap<>(); - - public void convertParameters( - Multimap> theParameters, FhirContext theFhirContext) { - if (theParameters == null) { - return; - } - separateParameterTypes(theParameters); - convertToSearchParameterMap(mySeparatedSearchParameters); - convertToStringMap(mySeparatedResultParameters, theFhirContext); - } - - public void convertToStringMap( - @Nonnull Multimap> theParameters, @Nonnull FhirContext theFhirContext) { - for (Map.Entry> entry : theParameters.entries()) { - String[] values = new String[entry.getValue().size()]; - for (int i = 0; i < entry.getValue().size(); i++) { - values[i] = entry.getValue().get(i).getValueAsQueryToken(theFhirContext); - } - myResultParameters.put(entry.getKey(), values); - } - } - - public void convertToSearchParameterMap(Multimap> theSearchMap) { - if (theSearchMap == null) { - return; - } - for (Map.Entry> entry : theSearchMap.entries()) { - // if list of parameters is the value - if (entry.getValue().size() > 1 && !isOrList(entry.getValue()) && !isAndList(entry.getValue())) { - // is value a TokenParam - addTokenToSearchIfNeeded(entry); - - // parameter type is single value list - } else { - for (IQueryParameterType value : entry.getValue()) { - setParameterTypeValue(entry.getKey(), value); - } - } - } - } - - private void addTokenToSearchIfNeeded(Map.Entry> theEntry) { - if (isTokenParam(theEntry.getValue().get(0))) { - String tokenKey = theEntry.getKey(); - TokenOrListParam tokenList = new TokenOrListParam(); - for (IQueryParameterType rec : theEntry.getValue()) { - tokenList.add((TokenParam) rec); - } - mySearchParameterMap.add(tokenKey, tokenList); - } - } - - public void setParameterTypeValue(@Nonnull String theKey, @Nonnull T theParameterType) { - if (isOrList(theParameterType)) { - mySearchParameterMap.add(theKey, (IQueryParameterOr) theParameterType); - } else if (isAndList(theParameterType)) { - mySearchParameterMap.add(theKey, (IQueryParameterAnd) theParameterType); - } else { - mySearchParameterMap.add(theKey, (IQueryParameterType) theParameterType); - } - } - - public void separateParameterTypes(@Nonnull Multimap> theParameters) { - for (Map.Entry> entry : theParameters.entries()) { - if (isSearchResultParameter(entry.getKey())) { - mySeparatedResultParameters.put(entry.getKey(), entry.getValue()); - } else { - mySeparatedSearchParameters.put(entry.getKey(), entry.getValue()); - } - } - } - - public boolean isSearchResultParameter(String theParameterName) { - return mySearchResultParameters.contains(theParameterName); - } - - public boolean isOrList(@Nonnull T theParameterType) { - return IQueryParameterOr.class.isAssignableFrom(theParameterType.getClass()); - } - - public boolean isAndList(@Nonnull T theParameterType) { - return IQueryParameterAnd.class.isAssignableFrom(theParameterType.getClass()); - } - - public boolean isTokenParam(@Nonnull T theParameterType) { - return TokenParam.class.isAssignableFrom(theParameterType.getClass()); - } -} diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/repository/searchparam/ISpecialParameterProcessor.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/repository/searchparam/ISpecialParameterProcessor.java new file mode 100644 index 000000000000..a5a00d0ae365 --- /dev/null +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/repository/searchparam/ISpecialParameterProcessor.java @@ -0,0 +1,24 @@ +package ca.uhn.fhir.jpa.repository.searchparam; + +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.model.api.IQueryParameterType; + +import java.util.List; + +/** + * Interface for processing special search parameters like "_count" and "_sort" + * that are not stored as AND/OR lists in the SearchParameterMap. + */ +interface ISpecialParameterProcessor { + static String paramAsQueryString(IQueryParameterType theParameter) { + return theParameter.getValueAsQueryToken(null); + } + + /** + * Apply this processor to the theValues and update the SearchParameterMap. + * @param theKey the key of the parameter being processed, e.g. "_sort" + * @param theValues the values of the parameter being processed, e.g. new TokenParam("-date") + * @param theSearchParameterMap the target to modify + */ + void process(String theKey, List theValues, SearchParameterMap theSearchParameterMap); +} diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/repository/searchparam/IncludeParameterProcessor.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/repository/searchparam/IncludeParameterProcessor.java new file mode 100644 index 000000000000..53a9f424a6ff --- /dev/null +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/repository/searchparam/IncludeParameterProcessor.java @@ -0,0 +1,37 @@ +package ca.uhn.fhir.jpa.repository.searchparam; + +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.model.api.IQueryParameterType; +import ca.uhn.fhir.model.api.Include; +import jakarta.annotation.Nonnull; + +import java.util.List; +import java.util.function.BiConsumer; + +class IncludeParameterProcessor implements ISpecialParameterProcessor { + private final boolean myIterationFlag; + private final BiConsumer mySetter; + + private IncludeParameterProcessor(boolean theIterationFlag, BiConsumer theSetter) { + myIterationFlag = theIterationFlag; + mySetter = theSetter; + } + + @Override + public void process(String theKey, List v, SearchParameterMap theSearchParameterMap) { + v.stream() + .map(ISpecialParameterProcessor::paramAsQueryString) + .map(s -> new Include(s, myIterationFlag)) + .forEach(i -> mySetter.accept(theSearchParameterMap, i)); + } + + @Nonnull + static IncludeParameterProcessor includeProcessor(boolean theIterationFlag) { + return new IncludeParameterProcessor(theIterationFlag, SearchParameterMap::addInclude); + } + + @Nonnull + static IncludeParameterProcessor revincludeProcessor(boolean theIterationFlag) { + return new IncludeParameterProcessor(theIterationFlag, SearchParameterMap::addRevInclude); + } +} diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/repository/searchparam/LastValueWinsParameterProcessor.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/repository/searchparam/LastValueWinsParameterProcessor.java new file mode 100644 index 000000000000..365b7fb7761b --- /dev/null +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/repository/searchparam/LastValueWinsParameterProcessor.java @@ -0,0 +1,48 @@ +package ca.uhn.fhir.jpa.repository.searchparam; + +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.model.api.IQueryParameterType; + +import java.util.List; +import java.util.function.BiConsumer; +import java.util.function.Function; + +import static com.google.common.base.Strings.isNullOrEmpty; + +/** + * A processor that takes the last value of a parameter and converts it to a type T while treating null or blank as null. + * @param the type used in the SearchParameterMap setter. + */ +class LastValueWinsParameterProcessor implements ISpecialParameterProcessor { + private final Function myConverter; + private final BiConsumer mySearchParameterMapSetter; + + LastValueWinsParameterProcessor( + Function theConverter, BiConsumer theSearchParameterMapSetter) { + myConverter = theConverter; + mySearchParameterMapSetter = theSearchParameterMapSetter; + } + + @Override + public void process(String k, List theValues, SearchParameterMap theSearchParameterMap) { + + String lastValue = theValues.stream() + .map(ISpecialParameterProcessor::paramAsQueryString) + .reduce(null, (l, r) -> r); + + T converted = isNullOrEmpty(lastValue) ? null : myConverter.apply(lastValue); + + mySearchParameterMapSetter.accept(theSearchParameterMap, converted); + } + + /** + * Build a processor that takes the last value of a parameter, converts it to a type, + * and sets a single value on the SearchParameterMap. + * Treats null or blank values as null. + * @param the type used in the SearchParameterMap setter. + */ + public static ISpecialParameterProcessor lastValueWins( + Function theConverter, BiConsumer theSearchParameterMapSetter) { + return new LastValueWinsParameterProcessor<>(theConverter, theSearchParameterMapSetter); + } +} diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/repository/searchparam/SearchParameterMapRepositoryRestQueryBuilder.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/repository/searchparam/SearchParameterMapRepositoryRestQueryBuilder.java new file mode 100644 index 000000000000..dac1f0492027 --- /dev/null +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/repository/searchparam/SearchParameterMapRepositoryRestQueryBuilder.java @@ -0,0 +1,121 @@ +package ca.uhn.fhir.jpa.repository.searchparam; + +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.model.api.IQueryParameterType; +import ca.uhn.fhir.repository.IRepository; +import ca.uhn.fhir.repository.IRepositoryRestQueryBuilder; +import ca.uhn.fhir.rest.api.SearchContainedModeEnum; +import ca.uhn.fhir.rest.api.SearchTotalModeEnum; +import ca.uhn.fhir.rest.api.SummaryEnum; +import jakarta.annotation.Nonnull; +import org.apache.commons.lang3.Validate; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static ca.uhn.fhir.jpa.repository.searchparam.IncludeParameterProcessor.includeProcessor; +import static ca.uhn.fhir.jpa.repository.searchparam.IncludeParameterProcessor.revincludeProcessor; +import static ca.uhn.fhir.jpa.repository.searchparam.LastValueWinsParameterProcessor.lastValueWins; +import static ca.uhn.fhir.rest.api.Constants.PARAM_CONTAINED; +import static ca.uhn.fhir.rest.api.Constants.PARAM_COUNT; +import static ca.uhn.fhir.rest.api.Constants.PARAM_INCLUDE; +import static ca.uhn.fhir.rest.api.Constants.PARAM_INCLUDE_ITERATE; +import static ca.uhn.fhir.rest.api.Constants.PARAM_INCLUDE_RECURSE; +import static ca.uhn.fhir.rest.api.Constants.PARAM_OFFSET; +import static ca.uhn.fhir.rest.api.Constants.PARAM_REVINCLUDE; +import static ca.uhn.fhir.rest.api.Constants.PARAM_REVINCLUDE_ITERATE; +import static ca.uhn.fhir.rest.api.Constants.PARAM_REVINCLUDE_RECURSE; +import static ca.uhn.fhir.rest.api.Constants.PARAM_SEARCH_TOTAL_MODE; +import static ca.uhn.fhir.rest.api.Constants.PARAM_SORT; +import static ca.uhn.fhir.rest.api.Constants.PARAM_SUMMARY; + +public class SearchParameterMapRepositoryRestQueryBuilder implements IRepositoryRestQueryBuilder { + /** Our unsupported specials */ + private static final Set ourIgnoredSpecials = Set.of( + "_elements", // implemented at the FHIR endpoint - not applicable in the repository + "_containedType" // unsupported by JPA repositories + ); + + final SearchParameterMap mySearchParameterMapResult = new SearchParameterMap(); + + SearchParameterMapRepositoryRestQueryBuilder() {} + + @Override + public IRepositoryRestQueryBuilder addOrList(String theParamName, List theOrList) { + Validate.notEmpty(theParamName, "theParamName"); + + if (ourIgnoredSpecials.contains(theParamName)) { + return this; + } + // Is this a special parameter like _sort, _count, etc.? + ISpecialParameterProcessor specialParameterProcessor = ourSpecialParamHandlers.get(theParamName); + if (specialParameterProcessor != null) { + specialParameterProcessor.process(theParamName, theOrList, mySearchParameterMapResult); + } else { + // this is a normal query parameter. + mySearchParameterMapResult.addOrList(theParamName, theOrList); + } + + return this; + } + + /** + * Main entry point for converting from the IRepository interfaces to a JPA SearchParameterMap. + * @param theQueryContributor the query callback to convert + * @return a SearchParameterMap for use with JPA repositories + */ + public static SearchParameterMap buildFromQueryContributor(IRepository.IRepositoryRestQueryContributor theQueryContributor) { + SearchParameterMap searchParameterMap; + // If the contributor is already a SearchParameterMap, use it directly. + // This allows pass-though of stuff like $everything that isn't part of the main rest-query syntax. + if (theQueryContributor instanceof SearchParameterMap theSp) { + searchParameterMap = theSp; + } else { + // Build a SearchParameterMap from scratch. + SearchParameterMapRepositoryRestQueryBuilder builder = new SearchParameterMapRepositoryRestQueryBuilder(); + theQueryContributor.contributeToQuery(builder); + searchParameterMap = builder.build(); + } + return searchParameterMap; + } + + public SearchParameterMap build() { + return mySearchParameterMapResult; + } + + /** + * Handlers for all the specials + */ + Map ourSpecialParamHandlers = buildHandlerTable(); + + @Nonnull + private static Map buildHandlerTable() { + Map lastWins = Map.of( + PARAM_CONTAINED, + lastValueWins(SearchContainedModeEnum::fromCode, SearchParameterMap::setSearchContainedMode), + PARAM_COUNT, lastValueWins(Integer::parseInt, SearchParameterMap::setCount), + PARAM_OFFSET, lastValueWins(Integer::parseInt, SearchParameterMap::setOffset), + PARAM_SUMMARY, lastValueWins(SummaryEnum::fromCode, SearchParameterMap::setSummaryMode), + PARAM_SEARCH_TOTAL_MODE, + lastValueWins(SearchTotalModeEnum::fromCode, SearchParameterMap::setSearchTotalMode)); + + Map includeProcessors = Map.of( + PARAM_INCLUDE, includeProcessor(false), + // fixme Include should be a normal IQueryParameterType + PARAM_INCLUDE_ITERATE, includeProcessor(true), + PARAM_INCLUDE_RECURSE, includeProcessor(true), + PARAM_REVINCLUDE, revincludeProcessor(false), + PARAM_REVINCLUDE_ITERATE, revincludeProcessor(true), + PARAM_REVINCLUDE_RECURSE, revincludeProcessor(true)); + + Map handlers = new HashMap<>(); + handlers.put(PARAM_SORT, new SortProcessor()); + handlers.putAll(lastWins); + handlers.putAll(includeProcessors); + + return Collections.unmodifiableMap(handlers); + } +} diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/repository/searchparam/SortProcessor.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/repository/searchparam/SortProcessor.java new file mode 100644 index 000000000000..97f13c67ee6c --- /dev/null +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/repository/searchparam/SortProcessor.java @@ -0,0 +1,27 @@ +package ca.uhn.fhir.jpa.repository.searchparam; + +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.model.api.IQueryParameterType; +import ca.uhn.fhir.rest.api.SortSpec; + +import java.util.List; +import java.util.function.Consumer; + +class SortProcessor implements ISpecialParameterProcessor { + @Override + public void process( + String theKey, List theSortItems, SearchParameterMap theSearchParameterMap) { + List sortSpecs = theSortItems.stream() + .map(ISpecialParameterProcessor::paramAsQueryString) + .map(SortSpec::fromR3OrLaterParameterValue) + .toList(); + + // SortSpec is an intrusive linked list, with the head as a bare pointer in the SearchParameterMap. + Consumer sortAppendAction = theSearchParameterMap::setSort; + for (SortSpec sortSpec : sortSpecs) { + sortAppendAction.accept(sortSpec); + // we append at the tail + sortAppendAction = sortSpec::setChain; + } + } +} diff --git a/hapi-fhir-storage/src/test/java/ca/uhn/fhir/jpa/repository/SearchConverterTest.java b/hapi-fhir-storage/src/test/java/ca/uhn/fhir/jpa/repository/SearchConverterTest.java deleted file mode 100644 index 9800a6bc840d..000000000000 --- a/hapi-fhir-storage/src/test/java/ca/uhn/fhir/jpa/repository/SearchConverterTest.java +++ /dev/null @@ -1,193 +0,0 @@ -package ca.uhn.fhir.jpa.repository; - -import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.model.api.IQueryParameterType; -import ca.uhn.fhir.rest.param.NumberAndListParam; -import ca.uhn.fhir.rest.param.NumberOrListParam; -import ca.uhn.fhir.rest.param.SpecialAndListParam; -import ca.uhn.fhir.rest.param.SpecialOrListParam; -import ca.uhn.fhir.rest.param.TokenAndListParam; -import ca.uhn.fhir.rest.param.TokenOrListParam; -import ca.uhn.fhir.rest.param.UriAndListParam; -import ca.uhn.fhir.rest.param.UriOrListParam; -import ca.uhn.fhir.rest.param.UriParam; -import com.google.common.collect.ArrayListMultimap; -import com.google.common.collect.Multimap; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; - -class SearchConverterTest { - private SearchConverter myFixture; - - @BeforeEach - void setupFixture() { - myFixture = new SearchConverter(); - } - - @Test - void isSearchParameterShouldReturnTrue() { - boolean result = myFixture.isSearchResultParameter("_elements"); - assertTrue(result); - } - - @Test - void isSearchParameterShouldReturnFalse() { - boolean result = myFixture.isSearchResultParameter("_id"); - assertFalse(result); - } - - @Test - void isOrListShouldReturnTrue() { - boolean uriOrList = myFixture.isOrList(new UriOrListParam()); - boolean numberOrList = myFixture.isOrList(new NumberOrListParam()); - boolean specialOrList = myFixture.isOrList(new SpecialOrListParam()); - boolean tokenOrList = myFixture.isOrList(new TokenOrListParam()); - assertTrue(uriOrList); - assertTrue(numberOrList); - assertTrue(specialOrList); - assertTrue(tokenOrList); - } - - @Test - void isAndListShouldReturnTrue() { - boolean uriAndList = myFixture.isAndList(new UriAndListParam()); - boolean numberAndList = myFixture.isAndList(new NumberAndListParam()); - boolean specialAndList = myFixture.isAndList(new SpecialAndListParam()); - boolean tokenAndList = myFixture.isAndList(new TokenAndListParam()); - assertTrue(uriAndList); - assertTrue(numberAndList); - assertTrue(specialAndList); - assertTrue(tokenAndList); - } - - @Test - void isOrListShouldReturnFalse() { - boolean uriAndList = myFixture.isOrList(new UriAndListParam()); - assertFalse(uriAndList); - } - - @Test - void isAndListShouldReturnFalse() { - boolean uriAndList = myFixture.isAndList(new UriOrListParam()); - assertFalse(uriAndList); - } - - @Test - void setParameterTypeValueShouldSetWithOrValue() { - String key = "theOrKey"; - UriOrListParam theValue = withUriOrListParam(); - myFixture.setParameterTypeValue(key, theValue); - String result = myFixture.mySearchParameterMap.toNormalizedQueryString(withFhirContext()); - String expected = "?theOrKey=theSecondValue,theValue"; - assertEquals(expected, result); - } - - @Test - void setParameterTypeValueShouldSetWithAndValue() { - String key = "theAndKey"; - UriAndListParam theValue = withUriAndListParam(); - myFixture.setParameterTypeValue(key, theValue); - String result = myFixture.mySearchParameterMap.toNormalizedQueryString(withFhirContext()); - String expected = "?theAndKey=theSecondValue,theValue&theAndKey=theSecondValueAgain,theValueAgain"; - assertEquals(expected, result); - } - - @Test - void setParameterTypeValueShouldSetWithBaseValue() { - String expected = "?key=theValue"; - UriParam theValue = new UriParam("theValue"); - String key = "key"; - myFixture.setParameterTypeValue(key, theValue); - String result = myFixture.mySearchParameterMap.toNormalizedQueryString(withFhirContext()); - assertEquals(expected, result); - } - - @Test - void separateParameterTypesShouldSeparateSearchAndResultParams() { - myFixture.separateParameterTypes(withParamList()); - assertThat(myFixture.mySeparatedSearchParameters.entries()).hasSize(2); - assertThat(myFixture.mySeparatedResultParameters.entries()).hasSize(3); - } - - @Test - void convertToStringMapShouldConvert() { - Map expected = withParamListAsStrings(); - myFixture.convertToStringMap(withParamList(), withFhirContext()); - Map result = myFixture.myResultParameters; - assertEquals(result.keySet(), expected.keySet()); - assertThat(result.entrySet().stream().allMatch(e -> Arrays.equals(e.getValue(), expected.get(e.getKey())))) - .isTrue(); - } - - Multimap> withParamList() { - ArrayListMultimap> paramList = ArrayListMultimap.create(); - paramList.put("_id", withUriParam(1)); - paramList.put("_elements", withUriParam(2)); - paramList.put("_lastUpdated", withUriParam(1)); - paramList.put("_total", withUriParam(1)); - paramList.put("_count", withUriParam(3)); - return paramList; - } - - Map withParamListAsStrings() { - Map paramList = new HashMap<>(); - paramList.put("_id", withStringParam(1)); - paramList.put("_elements", withStringParam(2)); - paramList.put("_lastUpdated", withStringParam(1)); - paramList.put("_total", withStringParam(1)); - paramList.put("_count", withStringParam(3)); - return paramList; - } - - List withUriParam(int theNumberOfParams) { - List paramList = new ArrayList<>(); - for (int i = 0; i < theNumberOfParams; i++) { - paramList.add(new UriParam(Integer.toString(i))); - } - return paramList; - } - - UriOrListParam withUriOrListParam() { - UriOrListParam orList = new UriOrListParam(); - orList.add(new UriParam("theValue")); - orList.add(new UriParam("theSecondValue")); - return orList; - } - - UriOrListParam withUriOrListParamSecond() { - UriOrListParam orList = new UriOrListParam(); - orList.add(new UriParam("theValueAgain")); - orList.add(new UriParam("theSecondValueAgain")); - return orList; - } - - UriAndListParam withUriAndListParam() { - UriAndListParam andList = new UriAndListParam(); - andList.addAnd(withUriOrListParam()); - andList.addAnd(withUriOrListParamSecond()); - return andList; - } - - String[] withStringParam(int theNumberOfParams) { - String[] paramList = new String[theNumberOfParams]; - for (int i = 0; i < theNumberOfParams; i++) { - paramList[i] = Integer.toString(i); - } - return paramList; - } - - FhirContext withFhirContext() { - return FhirContext.forR4Cached(); - } -} diff --git a/hapi-fhir-storage/src/test/java/ca/uhn/fhir/jpa/repository/searchparam/SearchParameterMapQueryBuilderTest.java b/hapi-fhir-storage/src/test/java/ca/uhn/fhir/jpa/repository/searchparam/SearchParameterMapQueryBuilderTest.java new file mode 100644 index 000000000000..b7dd6560029e --- /dev/null +++ b/hapi-fhir-storage/src/test/java/ca/uhn/fhir/jpa/repository/searchparam/SearchParameterMapQueryBuilderTest.java @@ -0,0 +1,184 @@ +package ca.uhn.fhir.jpa.repository.searchparam; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.model.api.Include; +import ca.uhn.fhir.repository.IRepository; +import ca.uhn.fhir.rest.api.SearchContainedModeEnum; +import ca.uhn.fhir.rest.api.SearchTotalModeEnum; +import ca.uhn.fhir.rest.api.SortOrderEnum; +import ca.uhn.fhir.rest.api.SummaryEnum; +import ca.uhn.fhir.rest.param.NumberParam; +import ca.uhn.fhir.rest.param.ReferenceParam; +import ca.uhn.fhir.rest.param.StringParam; +import ca.uhn.fhir.rest.param.TokenParam; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class SearchParameterMapQueryBuilderTest { + FhirContext myFhirContext = FhirContext.forR4Cached(); + SearchParameterMapRepositoryRestQueryBuilder myBuilder = new SearchParameterMapRepositoryRestQueryBuilder(); + SearchParameterMap myResult; + + @Test + void testBuilder_SPMapIsUsedAsIs() { + // given + SearchParameterMap spMap = new SearchParameterMap(); + + // when + SearchParameterMap result = SearchParameterMapRepositoryRestQueryBuilder.buildFromQueryContributor(spMap); + + // then + assertThat(result).isSameAs(spMap); + } + + @Test + void testBuilder_callbackBuildsSPMap() { + // given + IRepository.IRepositoryRestQueryContributor queryContributor = + builder -> builder.addNumericParameter("_count", 123); + + // when + SearchParameterMap result = SearchParameterMapRepositoryRestQueryBuilder.buildFromQueryContributor(queryContributor); + + // then + assertThat(result.toNormalizedQueryString(myFhirContext)).isEqualTo("?_count=123"); + } + + @Test + void testIdParameter() { + // when + myBuilder.addOrList("_id", new ReferenceParam("123"), new ReferenceParam("345")); + + // then + assertConvertsTo("?_id=123,345"); + } + + @Test + void testCountParameter() { + // when + myBuilder.addNumericParameter("_count", 123); + + myResult = myBuilder.build(); + + assertThat(myResult.getCount()).isEqualTo(123); + } + + + @Test + void testMultipleCountUsesLast() { + // when + myBuilder.addOrList("_count", new NumberParam("123"), new NumberParam("50")); + myBuilder.addOrList("_count", new NumberParam("23"), new NumberParam("42")); + + myResult = myBuilder.build(); + + assertThat(myResult.getCount()).isEqualTo(42); + } + + @Test + void testOffset() { + // given + myBuilder.addOrList("_offset", new NumberParam("900")); + + myResult = myBuilder.build(); + + assertThat(myResult.getOffset()).isEqualTo(900); + } + + @Test + void testSort() { + myBuilder.addOrList("_sort", new TokenParam("-date")); + + myResult = myBuilder.build(); + + assertThat(myResult.getSort().getParamName()).isEqualTo("date"); + assertThat(myResult.getSort().getOrder()).isEqualTo(SortOrderEnum.DESC); + } + + @Test + void testSortChain() { + myBuilder.addOrList("_sort", new TokenParam("-date"), new TokenParam("_id")); + + assertConvertsTo("?_sort=-date,_id"); + } + + + @Test + void testSummary() { + // given + myBuilder.addOrList("_summary", new TokenParam("count")); + + myResult = myBuilder.build(); + + assertThat(myResult.getSummaryMode()).isEqualTo(SummaryEnum.COUNT); + } + + @Test + void testTotal() { + // given + myBuilder.addOrList("_total", new TokenParam("accurate")); + + myResult = myBuilder.build(); + + assertThat(myResult.getSearchTotalMode()).isEqualTo(SearchTotalModeEnum.ACCURATE); + } + + @Test + void testContained() { + // given + myBuilder.addOrList("_contained", new TokenParam("true")); + + myResult = myBuilder.build(); + + assertThat(myResult.getSearchContainedMode()).isEqualTo(SearchContainedModeEnum.TRUE); + } + + @Test + void testInclude() { + // given + myBuilder.addOrList("_include", new StringParam("Patient:general-practitioner")); + + myResult = myBuilder.build(); + + assertThat(myResult.getIncludes()).contains(new Include("Patient:general-practitioner")); + } + + + // fixme we normally put modifiers on values, not keys.. + @Test + void testIncludeIterate() { + // given + myBuilder.addOrList("_include:iterate", new StringParam("Patient:general-practitioner")); + + myResult = myBuilder.build(); + + assertThat(myResult.getIncludes()).contains(new Include("Patient:general-practitioner", true)); + } + + @Test + void testRevinclude() { + // given + myBuilder.addOrList("_revinclude", new StringParam("Patient:general-practitioner")); + + myResult = myBuilder.build(); + + assertThat(myResult.getRevIncludes()).contains(new Include("Patient:general-practitioner")); + } + + @Test + void testRevincludeIterate() { + // given + myBuilder.addOrList("_revinclude:iterate", new StringParam("Patient:general-practitioner")); + + myResult = myBuilder.build(); + + assertThat(myResult.getRevIncludes()).contains(new Include("Patient:general-practitioner", true)); + } + + private void assertConvertsTo(String expected) { + myResult = myBuilder.build(); + assertThat(myResult.toNormalizedQueryString(myFhirContext)).isEqualTo(expected); + } +} diff --git a/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/repository/IRepositoryTest.java b/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/repository/IRepositoryTest.java index 3156b73e5aa3..2d58f8c41b19 100644 --- a/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/repository/IRepositoryTest.java +++ b/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/repository/IRepositoryTest.java @@ -16,6 +16,7 @@ import ca.uhn.fhir.util.ParametersUtil; import ca.uhn.fhir.util.bundle.BundleResponseEntryParts; import ca.uhn.fhir.util.bundle.SearchBundleEntryParts; +import com.google.common.collect.Multimaps; import org.hl7.fhir.instance.model.api.IBaseBundle; import org.hl7.fhir.instance.model.api.IBaseParameters; import org.hl7.fhir.instance.model.api.IBaseResource; @@ -279,7 +280,7 @@ default void testSearchAllOfType() { IBaseBundle bundle = new BundleBuilder(context).getBundle(); // when - IBaseBundle searchResult = repository.search(bundle.getClass(), patientClass, Map.of()); + IBaseBundle searchResult = repository.search(bundle.getClass(), patientClass, Multimaps.forMap(Map.of())); // then List entries = BundleUtil.getSearchBundleEntryParts(context, searchResult); @@ -302,7 +303,7 @@ default void testSearchById() { IBaseBundle searchResult = repository.search( bundle.getClass(), patientClass, - Map.of("_id", List.of(new ReferenceParam("abc"), new ReferenceParam("ghi")))); + Multimaps.forMap(Map.of("_id", List.of(new ReferenceParam("abc"), new ReferenceParam("ghi"))))); // then List entries = BundleUtil.getSearchBundleEntryParts(context, searchResult); @@ -332,15 +333,15 @@ private RepositoryTestDataBuilder getRepositoryTestDataBuilder() { } } - private IRepository getRepository() { + default IRepository getRepository() { return getRepositoryTestSupport().repository(); } - private ITestDataBuilder getTestDataBuilder() { + default ITestDataBuilder getTestDataBuilder() { return getRepositoryTestSupport().getRepositoryTestDataBuilder(); } - private FhirTerser getTerser() { + default FhirTerser getTerser() { return getRepositoryTestSupport().getFhirTerser(); } }