Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
5b410bb
Basic CRUD tests for repositories
michaelabuckley Jul 4, 2025
df57914
Basic CRUD tests for repositories
michaelabuckley Jul 4, 2025
9cb9ec5
warnings + cleanup
michaelabuckley Jul 4, 2025
f2721a5
Normalize ids
michaelabuckley Jul 4, 2025
8893689
Test patch
michaelabuckley Jul 4, 2025
9b84d6e
remove redundant not-implemented stubs.
michaelabuckley Jul 7, 2025
daa231d
InMemory test
michaelabuckley Jul 7, 2025
28bb118
Spotless
michaelabuckley Jul 7, 2025
fb53ff5
Spotless
michaelabuckley Jul 7, 2025
e25f115
start GenericClientRepository
michaelabuckley Jul 7, 2025
268a2e8
test GenericClientRepository
michaelabuckley Jul 7, 2025
e7609fc
Redundant override
michaelabuckley Jul 7, 2025
e37c382
Javadoc
michaelabuckley Jul 7, 2025
d1a80ad
House style
michaelabuckley Jul 8, 2025
182966d
Add url-based service loader.
michaelabuckley Jul 8, 2025
cc48e42
New parts record for bundle processing
michaelabuckley Jul 11, 2025
9657d8c
working tests
michaelabuckley Jul 11, 2025
b194263
Add sketch search to memory repository
michaelabuckley Jul 12, 2025
4ade296
Cleanup
michaelabuckley Jul 12, 2025
11b7d51
Revert "Bump to core 6.5.27 (#7094)"
michaelabuckley Jul 12, 2025
dedc60d
Merge remote-tracking branch 'origin/master' into mb-in-memory-reposi…
michaelabuckley Jul 12, 2025
53e4024
Fix status changes
michaelabuckley Jul 12, 2025
685aadb
Changelog
michaelabuckley Jul 12, 2025
c84829f
spotless
michaelabuckley Jul 12, 2025
0005000
fixmes
michaelabuckley Jul 12, 2025
109f7eb
Revert delete status changes
michaelabuckley Jul 12, 2025
4bfb9d8
Extract search logic
michaelabuckley Jul 12, 2025
c0cde90
spotless
michaelabuckley Jul 12, 2025
4dd58ed
polish
michaelabuckley Jul 12, 2025
f8c11b0
polish
michaelabuckley Jul 12, 2025
4fb2052
Move to memory package
michaelabuckley Jul 12, 2025
db7e704
Move to memory package
michaelabuckley Jul 12, 2025
f2955ce
Simplify delete
michaelabuckley Jul 12, 2025
ca43996
Simplify delete
michaelabuckley Jul 12, 2025
0c998d6
bikeshedding
michaelabuckley Jul 12, 2025
4a04afe
House style
michaelabuckley Jul 12, 2025
d638ee5
Revert to match animal sniffer
michaelabuckley Jul 12, 2025
360545b
Fix bad merge - sync with master
michaelabuckley Jul 12, 2025
10b0c5f
Output logging in InMemoryFhirRepository
michaelabuckley Jul 13, 2025
5f7aa74
spotless
michaelabuckley Jul 13, 2025
7a95abe
Update manual operation handling docs.
michaelabuckley Jul 13, 2025
dd435ee
Extract hapi-fhir-repositories module
michaelabuckley Jul 15, 2025
8973093
spotless
michaelabuckley Jul 15, 2025
bcf0c96
scope
michaelabuckley Jul 16, 2025
ee6c027
repackaging in hapi
michaelabuckley Jul 16, 2025
580f021
repackaging in hapi
michaelabuckley Jul 16, 2025
1bcd44e
review polish
michaelabuckley Jul 17, 2025
eeedb8e
Document limitations and add some transaction processing tests.
michaelabuckley Jul 18, 2025
d642463
Review feedback
michaelabuckley Jul 18, 2025
c0ca787
Review feedback
michaelabuckley Jul 18, 2025
5057b87
Review feedback
michaelabuckley Jul 18, 2025
41696b5
spotless
michaelabuckley Jul 18, 2025
97746d0
Merge branch 'refs/heads/master' into mb-hapi-fhir-repository-search
michaelabuckley Jul 18, 2025
c511e09
repository search
michaelabuckley Jul 23, 2025
0f5b10a
Start search contributor to make friends with SearchParameterMap
michaelabuckley Jul 30, 2025
4e38040
use computeIfAbsent
michaelabuckley Jul 30, 2025
9ac989b
Good start on SPMap converter
michaelabuckley Jul 30, 2025
5c06f21
Tiny parser for SortSpec
michaelabuckley Jul 30, 2025
bc7cc9a
Sort in builder
michaelabuckley Jul 30, 2025
be659b9
_include and _include:iterate
michaelabuckley Jul 30, 2025
9b7ce72
Cleanup to appease Sonar
michaelabuckley Jul 30, 2025
b82f2e7
Invoke an operation through repository!
michaelabuckley Jul 30, 2025
5ceb0c2
Merge remote-tracking branch 'origin/rel_8_4' into mb-hapi-fhir-repos…
michaelabuckley Jul 30, 2025
a164d53
remove stub test
michaelabuckley Jul 30, 2025
8bb726e
spotless
michaelabuckley Jul 31, 2025
4eea6c0
Implement SPMap -> Multimap
michaelabuckley Aug 1, 2025
eedf1e9
Implement SPMap -> Multimap
michaelabuckley Aug 1, 2025
4dfa815
Cleanup
michaelabuckley Aug 2, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -82,6 +83,15 @@
* level of support for interactions should be.
* </p>
*
* <p>
* 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.
* </p>
*
* @see <a href="https://www.hl7.org/fhir/http.html">FHIR REST API</a>
*/
@Beta
Expand Down Expand Up @@ -262,7 +272,9 @@ default <B extends IBaseBundle, T extends IBaseResource> 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 extends IBaseBundle, T extends IBaseResource> B search(
Class<B> bundleType, Class<T> resourceType, Map<String, List<IQueryParameterType>> searchParameters) {
return this.search(bundleType, resourceType, searchParameters, Collections.emptyMap());
Expand All @@ -275,18 +287,48 @@ default <B extends IBaseBundle, T extends IBaseResource> B search(
*
* @param <B> a Bundle type
* @param <T> 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 extends IBaseBundle, T extends IBaseResource> B search(
Class<B> bundleType,
Class<T> resourceType,
Multimap<String, List<IQueryParameterType>> searchParameters,
Map<String, String> headers);
default <B extends IBaseBundle, T extends IBaseResource> B search(
Class<B> theBundleType,
Class<T> theResourceType,
Multimap<String, List<IQueryParameterType>> theSearchParameters,
Map<String, String> 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 <a href="https://www.hl7.org/fhir/http.html#search">FHIR search</a>
*
* @param <B> a Bundle type
* @param <T> 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 extends IBaseBundle, T extends IBaseResource> B search(
Class<B> theBundleType,
Class<T> theResourceType,
IRepositoryRestQueryContributor theQueryContributor,
Map<String, String> 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
*
Expand All @@ -299,7 +341,9 @@ <B extends IBaseBundle, T extends IBaseResource> 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 extends IBaseBundle, T extends IBaseResource> B search(
Class<B> bundleType,
Class<T> resourceType,
Expand Down Expand Up @@ -737,4 +781,12 @@ default <B extends IBaseBundle, P extends IBaseParameters, I extends IIdType> B
private static <T> 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);
}
}
Original file line number Diff line number Diff line change
@@ -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<IQueryParameterType> 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<String, List<IQueryParameterType>> theSearchParameters) {
theSearchParameters.entries().forEach(e -> this.addOrList(e.getKey(), e.getValue()));
return this;
}

default IRepositoryRestQueryBuilder addAll(Map<String, List<IQueryParameterType>> theSearchParameters) {
theSearchParameters.forEach(this::addOrList);
return this;
}

}
Original file line number Diff line number Diff line change
@@ -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<String, List<IQueryParameterType>> mySearchParameters = ArrayListMultimap.create();

@Override
public IRepositoryRestQueryBuilder addOrList(String theParamName, List<IQueryParameterType> theParameters) {
validateHomogeneousList(theParamName, theParameters);
mySearchParameters.put(theParamName, theParameters);
return this;
}

private void validateHomogeneousList(String theName, List<IQueryParameterType> 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<String, List<IQueryParameterType>> 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<String, List<IQueryParameterType>> contributorToMultimap(
IRepository.IRepositoryRestQueryContributor theSearchQueryBuilder) {
MultiMapRepositoryRestQueryBuilder builder = new MultiMapRepositoryRestQueryBuilder();
theSearchQueryBuilder.contributeToQuery(builder);
return builder.toMultiMap();
}
}
53 changes: 53 additions & 0 deletions hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/SortSpec.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand All @@ -29,6 +33,7 @@
*/
public class SortSpec implements Serializable {

@Serial
private static final long serialVersionUID = 2866833099879713467L;

private SortSpec myChain;
Expand Down Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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<String, List<IQueryParameterType>> 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<String, List<IQueryParameterType>> 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);
}

}
Loading
Loading