Skip to content

Commit fcc0d45

Browse files
Provide a repository ServiceLoader-based factory, and an InMemoryFhirRepository for testing (#7104)
dd an in-memory implementation of our IRepository interface, and a generic test for implementations. Also add a ServiceLoader path to resolve IRepository implementations via a url scheme starting with fhir-repository: Introduce new hapi-fhir-repositories project with implementations of IRepository. Template test IRepositoryTest for basic CRUDS tests InMemoryRepository for testing GenericClient repository HapiFhirRepository for JPA Add url-based ServiceLoader - see Repositories Update manual operation handling docs.
1 parent bc7095d commit fcc0d45

File tree

45 files changed

+2945
-71
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+2945
-71
lines changed

hapi-fhir-base/src/main/java/ca/uhn/fhir/context/BaseRuntimeElementDefinition.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,10 +163,12 @@ public boolean isStandardType() {
163163
return myStandardType;
164164
}
165165

166+
@Nonnull
166167
public T newInstance() {
167168
return newInstance(null);
168169
}
169170

171+
@Nonnull
170172
public T newInstance(Object theArgument) {
171173
try {
172174
if (theArgument == null) {

hapi-fhir-base/src/main/java/ca/uhn/fhir/repository/IRepository.java

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,9 @@
2828
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
2929
import ca.uhn.fhir.rest.server.exceptions.NotImplementedOperationException;
3030
import com.google.common.annotations.Beta;
31-
import com.google.common.collect.ArrayListMultimap;
3231
import com.google.common.collect.Multimap;
32+
import com.google.common.collect.Multimaps;
33+
import jakarta.annotation.Nonnull;
3334
import org.hl7.fhir.instance.model.api.IBaseBundle;
3435
import org.hl7.fhir.instance.model.api.IBaseConformance;
3536
import org.hl7.fhir.instance.model.api.IBaseParameters;
@@ -304,16 +305,13 @@ default <B extends IBaseBundle, T extends IBaseResource> B search(
304305
Class<T> resourceType,
305306
Map<String, List<IQueryParameterType>> searchParameters,
306307
Map<String, String> headers) {
307-
ArrayListMultimap<String, List<IQueryParameterType>> multimap = ArrayListMultimap.create();
308-
searchParameters.forEach(multimap::put);
309-
return this.search(bundleType, resourceType, multimap, headers);
308+
return this.search(bundleType, resourceType, Multimaps.forMap(searchParameters), headers);
310309
}
311310

312311
// Paging starts here
313312

314313
/**
315314
* Reads a Bundle from a link on this repository
316-
*
317315
* This is typically used for paging during searches
318316
*
319317
* @see <a href="https://www.hl7.org/fhir/bundle-definitions.html#Bundle.link">FHIR Bundle
@@ -329,7 +327,6 @@ default <B extends IBaseBundle> B link(Class<B> bundleType, String url) {
329327

330328
/**
331329
* Reads a Bundle from a link on this repository
332-
*
333330
* This is typically used for paging during searches
334331
*
335332
* @see <a href="https://www.hl7.org/fhir/bundle-definitions.html#Bundle.link">FHIR Bundle
@@ -502,8 +499,10 @@ default <R extends IBaseResource, P extends IBaseParameters, T extends IBaseReso
502499
* @param returnType the class of the Resource the operation returns
503500
* @return the results of the operation
504501
*/
505-
<R extends IBaseResource, P extends IBaseParameters, T extends IBaseResource> R invoke(
506-
Class<T> resourceType, String name, P parameters, Class<R> returnType, Map<String, String> headers);
502+
default <R extends IBaseResource, P extends IBaseParameters, T extends IBaseResource> R invoke(
503+
Class<T> resourceType, String name, P parameters, Class<R> returnType, Map<String, String> headers) {
504+
return throwNotImplementedOperationException("type-level invoke is not supported by this repository");
505+
}
507506

508507
/**
509508
* Invokes a type-level operation on this repository
@@ -574,8 +573,10 @@ default <R extends IBaseResource, P extends IBaseParameters, I extends IIdType>
574573
* @param headers headers for this request, typically key-value pairs of HTTP headers
575574
* @return the results of the operation
576575
*/
577-
<R extends IBaseResource, P extends IBaseParameters, I extends IIdType> R invoke(
578-
I id, String name, P parameters, Class<R> returnType, Map<String, String> headers);
576+
default <R extends IBaseResource, P extends IBaseParameters, I extends IIdType> R invoke(
577+
I id, String name, P parameters, Class<R> returnType, Map<String, String> headers) {
578+
return throwNotImplementedOperationException("instance-level invoke is not supported by this repository");
579+
}
579580

580581
/**
581582
* Invokes an instance-level operation on this repository
@@ -721,7 +722,7 @@ default <B extends IBaseBundle, P extends IBaseParameters, I extends IIdType> B
721722

722723
/**
723724
* Returns the {@link FhirContext} used by the repository
724-
*
725+
* <p>
725726
* Practically, implementing FHIR functionality with the HAPI toolset requires a FhirContext. In
726727
* particular for things like version independent code. Ideally, a user could which FHIR version a
727728
* repository was configured for using things like the CapabilityStatement. In practice, that's
@@ -730,6 +731,7 @@ default <B extends IBaseBundle, P extends IBaseParameters, I extends IIdType> B
730731
*
731732
* @return a FhirContext
732733
*/
734+
@Nonnull
733735
FhirContext fhirContext();
734736

735737
private static <T> T throwNotImplementedOperationException(String theMessage) {
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package ca.uhn.fhir.repository;
2+
3+
import ca.uhn.fhir.context.FhirContext;
4+
import com.google.common.annotations.Beta;
5+
import jakarta.annotation.Nonnull;
6+
7+
import java.util.Optional;
8+
9+
/**
10+
* Service provider interface for loading repositories based on a URL.
11+
* Unstable API. Subject to change in future releases.
12+
* <p>
13+
* Implementors will receive the url parsed into IRepositoryRequest,
14+
* and dispatch of the <code>subScheme</code> property.
15+
* E.g. The InMemoryFhirRepositoryLoader will handle URLs
16+
* that start with <code>fhir-repository:memory:</code>.
17+
*/
18+
@Beta()
19+
public interface IRepositoryLoader {
20+
/**
21+
* Impelmentors should return true if they can handle the given URL.
22+
* @param theRepositoryRequest containing the URL to check
23+
* @return true if supported
24+
*/
25+
boolean canLoad(@Nonnull IRepositoryRequest theRepositoryRequest);
26+
27+
/**
28+
* Construct a version of {@link IRepository} based on the given URL.
29+
* Implementors can assume that the request passed the canLoad() check.
30+
*
31+
* @param theRepositoryRequest the details of the repository to load.
32+
* @return a repository instance
33+
*/
34+
@Nonnull
35+
IRepository loadRepository(@Nonnull IRepositoryRequest theRepositoryRequest);
36+
37+
interface IRepositoryRequest {
38+
/**
39+
* Get the full URL of the repository provided by the user.
40+
* @return the URL
41+
*/
42+
String getUrl();
43+
44+
/**
45+
* Get the sub-scheme of the URL, e.g. "memory" for "fhir-repository:memory:details".
46+
* @return the sub-scheme
47+
*/
48+
String getSubScheme();
49+
50+
/**
51+
* Get any additional details provided by the user in the URL.
52+
* This may be a url, a unique identifier for the repository, or configuration details.
53+
* @return the details
54+
*/
55+
String getDetails();
56+
57+
Optional<FhirContext> getFhirContext();
58+
}
59+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package ca.uhn.fhir.repository;
2+
3+
import ca.uhn.fhir.context.FhirContext;
4+
import ca.uhn.fhir.repository.impl.UrlRepositoryFactory;
5+
import jakarta.annotation.Nonnull;
6+
7+
/**
8+
* Static factory methods for creating instances of {@link IRepository}.
9+
*/
10+
public class Repositories {
11+
/**
12+
* Private constructor to prevent instantiation.
13+
*/
14+
Repositories() {}
15+
16+
public static boolean isRepositoryUrl(String theBaseUrl) {
17+
return UrlRepositoryFactory.isRepositoryUrl(theBaseUrl);
18+
}
19+
20+
/**
21+
* Constructs a version of {@link IRepository} based on the given URL.
22+
* These URLs are expected to be in the form of fhir-repository:subscheme:details.
23+
* Currently supported subschemes include:
24+
* <ul>
25+
* <li>memory - e.g. fhir-repository:memory:my-repo - the last piece (my-repo) identifies the repository</li>
26+
* </ul>
27+
* <p>
28+
* The subscheme is used to find a matching {@link IRepositoryLoader} implementation.
29+
*
30+
* @param theFhirContext the FHIR context to use for the repository.
31+
* @param theRepositoryUrl a url of the form fhir-repository:subscheme:details
32+
* @return a repository instance
33+
* @throws IllegalArgumentException if the URL is not a valid repository URL, or no loader can be found for the URL.
34+
*/
35+
@Nonnull
36+
public static IRepository repositoryForUrl(@Nonnull FhirContext theFhirContext, @Nonnull String theRepositoryUrl) {
37+
return UrlRepositoryFactory.buildRepository(theFhirContext, theRepositoryUrl);
38+
}
39+
}
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
package ca.uhn.fhir.repository.impl;
2+
3+
import ca.uhn.fhir.context.FhirContext;
4+
import ca.uhn.fhir.i18n.Msg;
5+
import ca.uhn.fhir.repository.IRepository;
6+
import ca.uhn.fhir.repository.IRepositoryLoader;
7+
import ca.uhn.fhir.repository.IRepositoryLoader.IRepositoryRequest;
8+
import ca.uhn.fhir.util.Logs;
9+
import com.google.common.annotations.Beta;
10+
import jakarta.annotation.Nonnull;
11+
import jakarta.annotation.Nullable;
12+
import org.slf4j.Logger;
13+
14+
import java.util.Objects;
15+
import java.util.Optional;
16+
import java.util.ServiceLoader;
17+
import java.util.regex.Matcher;
18+
import java.util.regex.Pattern;
19+
20+
/**
21+
* Use ServiceLoader to load {@link IRepositoryLoader} implementations
22+
* and provide chain-of-responsibility style matching by url to build IRepository instances.
23+
*/
24+
@Beta()
25+
public class UrlRepositoryFactory {
26+
private static final Logger ourLog = Logs.getRepositoryTroubleshootingLog();
27+
28+
public static final String FHIR_REPOSITORY_URL_SCHEME = "fhir-repository:";
29+
static final Pattern ourUrlPattern = Pattern.compile("^fhir-repository:([A-Za-z-]+):(.*)");
30+
31+
public static boolean isRepositoryUrl(String theBaseUrl) {
32+
return theBaseUrl != null
33+
&& theBaseUrl.startsWith(FHIR_REPOSITORY_URL_SCHEME)
34+
&& ourUrlPattern.matcher(theBaseUrl).matches();
35+
}
36+
37+
/**
38+
* Find a factory for {@link IRepository} based on the given URL.
39+
* This URL is expected to be in the form of fhir-repository:subscheme:details.
40+
* The subscheme is used to find a matching {@link IRepositoryLoader} implementation.
41+
*
42+
* @param theFhirContext the FHIR context to use for the repository, if required.
43+
* @param theRepositoryUrl a url of the form fhir-repository:subscheme:details
44+
* @return a repository instance
45+
* @throws IllegalArgumentException if the URL is not a valid repository URL, or no loader can be found for the URL.
46+
*/
47+
@Nonnull
48+
public static IRepository buildRepository(@Nullable FhirContext theFhirContext, @Nonnull String theRepositoryUrl) {
49+
ourLog.debug("Loading repository for url: {}", theRepositoryUrl);
50+
Objects.requireNonNull(theRepositoryUrl);
51+
52+
if (!isRepositoryUrl(theRepositoryUrl)) {
53+
throw new IllegalArgumentException(
54+
Msg.code(2737) + "Base URL is not a valid repository URL: " + theRepositoryUrl);
55+
}
56+
57+
ServiceLoader<IRepositoryLoader> load = ServiceLoader.load(IRepositoryLoader.class);
58+
IRepositoryRequest request = buildRequest(theRepositoryUrl, theFhirContext);
59+
for (IRepositoryLoader nextLoader : load) {
60+
logLoaderDetails(nextLoader);
61+
if (nextLoader.canLoad(request)) {
62+
ourLog.debug(
63+
"Loader {} can handle URL: {}. Instantiating repository.",
64+
nextLoader.getClass().getName(),
65+
theRepositoryUrl);
66+
return nextLoader.loadRepository(request);
67+
}
68+
}
69+
throw new IllegalArgumentException(
70+
Msg.code(2738) + "Unable to find a repository loader for URL: " + theRepositoryUrl);
71+
}
72+
73+
private static void logLoaderDetails(IRepositoryLoader nextLoader) {
74+
Class<? extends IRepositoryLoader> clazz = nextLoader.getClass();
75+
ourLog.debug(
76+
"Checking repository loader {} from {}",
77+
clazz.getName(),
78+
clazz.getProtectionDomain().getCodeSource().getLocation());
79+
}
80+
81+
/**
82+
* Builder for our abstract {@link IRepositoryRequest} interface.
83+
* @param theBaseUrl the fhir-repository URL to parse, e.g. fhir-repository:memory:my-repo
84+
* @param theFhirContext the FHIR context to use for the repository, if required.
85+
*/
86+
@Nonnull
87+
public static IRepositoryRequest buildRequest(@Nonnull String theBaseUrl, @Nullable FhirContext theFhirContext) {
88+
Matcher matcher = ourUrlPattern.matcher(theBaseUrl);
89+
String subScheme = null;
90+
String details = null;
91+
boolean found = matcher.matches();
92+
if (found) {
93+
subScheme = matcher.group(1);
94+
details = matcher.group(2);
95+
}
96+
97+
return new RepositoryRequest(theBaseUrl, subScheme, details, theFhirContext);
98+
}
99+
100+
/**
101+
* Internal implementation of {@link IRepositoryRequest}.
102+
*/
103+
record RepositoryRequest(String url, String subScheme, String details, FhirContext fhirContext)
104+
implements IRepositoryRequest {
105+
@Override
106+
public String getUrl() {
107+
return url;
108+
}
109+
110+
@Override
111+
public String getSubScheme() {
112+
return subScheme;
113+
}
114+
115+
@Override
116+
public String getDetails() {
117+
return details;
118+
}
119+
120+
@SuppressWarnings("java:S6211")
121+
@Override
122+
public Optional<FhirContext> getFhirContext() {
123+
return Optional.ofNullable(fhirContext);
124+
}
125+
}
126+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/**
2+
* This package provides an interface and implementations abstracting
3+
* access to a FHIR repository.
4+
* Use the {@link ca.uhn.fhir.repository.Repositories} class to create instances.
5+
* Implementations are in the hapi-fhir-repositories module.
6+
* A troubleshooting log is available via {@link ca.uhn.fhir.util.Logs#getRepositoryTroubleshootingLog()}.
7+
*/
8+
package ca.uhn.fhir.repository;

hapi-fhir-base/src/main/java/ca/uhn/fhir/util/BundleBuilder.java

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -495,10 +495,15 @@ public void addMessageEntry(IBaseResource theResource) {
495495
*/
496496
public IBase addEntry() {
497497
IBase entry = myEntryDef.newInstance();
498-
myEntryChild.getMutator().addValue(myBundle, entry);
498+
addEntry(entry);
499499
return entry;
500500
}
501501

502+
public IBase addEntry(IBase theEntry) {
503+
myEntryChild.getMutator().addValue(myBundle, theEntry);
504+
return theEntry;
505+
}
506+
502507
/**
503508
* Creates new search instance for the specified entry.
504509
* Note that this method does not work for DSTU2 model classes, it will only work
@@ -678,6 +683,19 @@ public void addProfile(String theProfile) {
678683
terser.addElement(myBundle, "Bundle.meta.profile", theProfile);
679684
}
680685

686+
public IBase addSearchMatchEntry(IBaseResource theResource) {
687+
setType("searchset");
688+
689+
IBase entry = addEntry();
690+
// Bundle.entry.resource
691+
myEntryResourceChild.getMutator().setValue(entry, theResource);
692+
// Bundle.entry.search
693+
IBase search = addSearch(entry);
694+
setSearchField(search, "mode", "match");
695+
696+
return entry;
697+
}
698+
681699
public class DeleteBuilder extends BaseOperationBuilder {
682700

683701
// nothing yet

0 commit comments

Comments
 (0)