Skip to content

Commit 35cc4d0

Browse files
Add a repository loader for :exp-kalm-filesystem: repositories (#7150)
1 parent 1b615b5 commit 35cc4d0

File tree

7 files changed

+208
-1
lines changed

7 files changed

+208
-1
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
type: add
3+
issue: 7150
4+
title: "Add a repository loader for fhir-repository:exp-kalm-filesystem:/path/to/directory repositories used by the KALM IDE."

hapi-fhir-repositories/pom.xml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,13 @@
4848
<version>${project.version}</version>
4949
<scope>test</scope>
5050
</dependency>
51-
51+
<!-- Used to compile the KalmFilesystemRepository. Delete this once KalmFilesystemRepositoryLoader is downstreamed to cqf or the IgRepository upstreamed here -->
52+
<dependency>
53+
<groupId>org.opencds.cqf.fhir</groupId>
54+
<artifactId>cqf-fhir-utility</artifactId>
55+
<version>3.23.0</version>
56+
<scope>provided</scope>
57+
</dependency>
5258
</dependencies>
5359

5460
<build>
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/*-
2+
* #%L
3+
* Smile CDR - CDR
4+
* %%
5+
* Copyright (C) 2016 - 2025 Smile CDR, Inc.
6+
* %%
7+
* All rights reserved.
8+
* #L%
9+
*/
10+
package ca.uhn.fhir.repository.impl.kalm;
11+
12+
import ca.uhn.fhir.context.FhirContext;
13+
import ca.uhn.fhir.i18n.Msg;
14+
import ca.uhn.fhir.repository.IRepository;
15+
import ca.uhn.fhir.repository.IRepositoryLoader;
16+
import ca.uhn.fhir.repository.impl.BaseSchemeBasedFhirRepositoryLoader;
17+
import jakarta.annotation.Nonnull;
18+
import org.apache.commons.lang3.Validate;
19+
20+
import java.io.File;
21+
import java.lang.reflect.Constructor;
22+
import java.lang.reflect.InvocationTargetException;
23+
import java.nio.file.Path;
24+
import java.util.Optional;
25+
26+
/**
27+
* ServiceLoader for the fhir-repository: url scheme.
28+
* Placeholder until this can move to cqf-fhir-utility or hapi-fhir-repositories.
29+
* @see ca.uhn.fhir.repository.Repositories#repositoryForUrl
30+
* TODO Either push this down to cqf-fhir-utility or upstream the IgRepository here to hapi-fhir-repositories.
31+
*/
32+
public class KalmFilesystemRepositoryLoader extends BaseSchemeBasedFhirRepositoryLoader implements IRepositoryLoader {
33+
public static final String URL_SUB_SCHEME = "exp-kalm-filesystem";
34+
35+
public KalmFilesystemRepositoryLoader() {
36+
super(URL_SUB_SCHEME);
37+
}
38+
39+
@Nonnull
40+
@Override
41+
public IRepository loadRepository(@Nonnull IRepositoryRequest theRepositoryRequest) {
42+
// Validate
43+
Optional<FhirContext> maybeContext = theRepositoryRequest.getFhirContext();
44+
Validate.isTrue(maybeContext.isPresent(), "The :%s: FHIR repository requires a FhirContext.", URL_SUB_SCHEME);
45+
46+
// we expect the details to be a file path for now.
47+
Path configPath = getConfigPath(theRepositoryRequest.getDetails());
48+
49+
return makeKalmRepository(maybeContext.get(), configPath);
50+
}
51+
52+
static Path getConfigPath(String theDetails) {
53+
Path configPath = Path.of(theDetails);
54+
File directory = configPath.toFile();
55+
56+
if (!(directory.exists() && directory.isDirectory())) {
57+
throw new IllegalArgumentException(
58+
Msg.code(2754) + "The provided path does not exist or is not a directory: " + directory);
59+
}
60+
return configPath;
61+
}
62+
63+
static IRepository makeKalmRepository(FhirContext theFhirContext, Path thePath) {
64+
try {
65+
// we use reflection in case cqf-utilities is not on the classpath. We don't want to force it as a
66+
// dependency.
67+
Class<?> repositoryClass = KalmFilesystemRepositoryLoader.class
68+
.getClassLoader()
69+
.loadClass("ca.uhn.fhir.repository.impl.kalm.PatchedKalmFileSystemRepository");
70+
Constructor<?> constructor = repositoryClass.getConstructor(FhirContext.class, Path.class);
71+
return (IRepository) constructor.newInstance(theFhirContext, thePath);
72+
} catch (ClassNotFoundException e) {
73+
throw new IllegalStateException(Msg.code(2752) + "Can't find IgRepository from cqf-utilities.jar", e);
74+
} catch (NoSuchMethodException
75+
| InvocationTargetException
76+
| InstantiationException
77+
| IllegalAccessException e) {
78+
throw new IllegalStateException(Msg.code(2753) + "Error building IgRepository.", e);
79+
}
80+
}
81+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*-
2+
* #%L
3+
* Smile CDR - CDR
4+
* %%
5+
* Copyright (C) 2016 - 2025 Smile CDR, Inc.
6+
* %%
7+
* All rights reserved.
8+
* #L%
9+
*/
10+
package ca.uhn.fhir.repository.impl.kalm;
11+
12+
import ca.uhn.fhir.context.FhirContext;
13+
import ca.uhn.fhir.repository.impl.NaiveRepositoryTransactionProcessor;
14+
import org.hl7.fhir.instance.model.api.IBaseBundle;
15+
import org.opencds.cqf.fhir.utility.repository.ig.IgRepository;
16+
17+
import java.nio.file.Path;
18+
import java.util.Map;
19+
20+
/**
21+
* A patched version of the IgRepository that adds support for transaction bundle processing.
22+
* The current 3.23 version from cqf-fhir-utility has a nop implementation of transaction().
23+
* TODO Delete this once the cfq implementation adds transaction support
24+
*/
25+
public class PatchedKalmFileSystemRepository extends IgRepository {
26+
public PatchedKalmFileSystemRepository(FhirContext theFhirContext, Path theRoot) {
27+
super(theFhirContext, theRoot);
28+
}
29+
30+
@Override
31+
public <B extends IBaseBundle> B transaction(B theTransactionBundle, Map<String, String> theHeaders) {
32+
return new NaiveRepositoryTransactionProcessor(this).processTransaction(theTransactionBundle);
33+
}
34+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/**
2+
* Loader support for the IgRepository from the Clinical Reasoning group.
3+
* This is a placeholder until this can move to cqf-fhir-utility or the code is up-streamed here to hapi-fhir-repositories.
4+
*/
5+
package ca.uhn.fhir.repository.impl.kalm;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
ca.uhn.fhir.repository.impl.memory.InMemoryFhirRepositoryLoader
2+
ca.uhn.fhir.repository.impl.kalm.KalmFilesystemRepositoryLoader
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package ca.uhn.fhir.repository.impl.kalm;
2+
3+
import ca.uhn.fhir.context.FhirContext;
4+
import ca.uhn.fhir.context.FhirVersionEnum;
5+
import ca.uhn.fhir.repository.impl.UrlRepositoryFactory;
6+
import org.junit.jupiter.api.Test;
7+
import org.junit.jupiter.api.io.TempDir;
8+
import org.mockito.Answers;
9+
import org.mockito.Mock;
10+
import org.mockito.Mockito;
11+
import org.mockito.junit.jupiter.MockitoSettings;
12+
13+
import java.io.IOException;
14+
import java.nio.file.Path;
15+
16+
import static org.assertj.core.api.Assertions.assertThat;
17+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
18+
19+
@MockitoSettings
20+
class KalmFilesystemRepositoryLoaderTest {
21+
@Mock(stubOnly = true, answer = Answers.RETURNS_DEEP_STUBS)
22+
FhirContext myFhirContext;
23+
@TempDir
24+
Path myTempDir;
25+
26+
27+
@Test
28+
void testRepositoryCreation() {
29+
// given
30+
KalmFilesystemRepositoryLoader loader = new KalmFilesystemRepositoryLoader();
31+
Mockito.when(myFhirContext.getVersion().getVersion()).thenReturn(FhirVersionEnum.R4);
32+
33+
// when
34+
var repo = loader.loadRepository(UrlRepositoryFactory.buildRequest("fhir-repository:exp-kalm-filesystem:" + myTempDir, myFhirContext));
35+
36+
// then
37+
assertThat(repo).isInstanceOf(PatchedKalmFileSystemRepository.class);
38+
}
39+
40+
@Test
41+
void testPathExists() {
42+
// given
43+
44+
// when
45+
Path configPath = KalmFilesystemRepositoryLoader.getConfigPath(myTempDir.toAbsolutePath().toString());
46+
47+
// then
48+
assertThat(configPath).isNotNull();
49+
}
50+
51+
@Test
52+
void testPathNotExists_error() {
53+
// given
54+
String nonexistent = myTempDir.resolve("nonexistent").toAbsolutePath().toString();
55+
56+
// when
57+
assertThatThrownBy(()->KalmFilesystemRepositoryLoader.getConfigPath(nonexistent))
58+
.isInstanceOf(IllegalArgumentException.class)
59+
.hasMessage("HAPI-2754: The provided path does not exist or is not a directory: " + nonexistent);
60+
}
61+
62+
@Test
63+
void testPathNotDirectory_error() throws IOException {
64+
// given
65+
Path filename = myTempDir.resolve("file").toAbsolutePath();
66+
filename.toFile().createNewFile(); // create a file instead of a directory
67+
String filenameString = filename.toString();
68+
69+
// when
70+
assertThatThrownBy(()->KalmFilesystemRepositoryLoader.getConfigPath(filenameString))
71+
.isInstanceOf(IllegalArgumentException.class)
72+
.hasMessage("HAPI-2754: The provided path does not exist or is not a directory: " + filename);
73+
}
74+
75+
76+
}

0 commit comments

Comments
 (0)