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();
}
}