Merge remote-tracking branch 'origin/master' into pw/searchparam-reindex-configuration

# Conflicts:
#	src/main/java/ca/uhn/fhir/jpa/starter/AppProperties.java
This commit is contained in:
Patrick Werner
2025-10-30 10:18:58 +01:00
7 changed files with 554 additions and 10 deletions

View File

@@ -49,6 +49,21 @@ docker run -p 8080:8080 -e hapi.fhir.default_encoding=xml hapiproject/hapi:lates
HAPI looks in the environment variables for properties in the [application.yaml](https://github.com/hapifhir/hapi-fhir-jpaserver-starter/blob/master/src/main/resources/application.yaml) file for defaults.
### Binary storage configuration
To stream large `Binary` payloads to disk instead of the database, configure the starter with filesystem storage properties:
```
hapi:
fhir:
binary_storage_enabled: true
binary_storage_mode: FILESYSTEM
binary_storage_filesystem_base_directory: /binstore
# inline_resource_storage_below_size: 131072 # optional override
```
When `binary_storage_mode` is set to `FILESYSTEM` and `inline_resource_storage_below_size` is omitted, the starter automatically applies a 102400 byte (100 KB) inline threshold so smaller payloads remain in the database. Ensure the directory you point to is writable by the process (for Docker builds, mount it into the container with appropriate permissions).
### Configuration via overridden application.yaml file and using Docker
You can customize HAPI by telling HAPI to look for the configuration file in a different location, e.g.:

View File

@@ -61,7 +61,15 @@ public class AppProperties {
private Boolean filter_search_enabled = true;
private Boolean graphql_enabled = false;
private Boolean binary_storage_enabled = false;
private Integer inline_resource_storage_below_size = 0;
public enum BinaryStorageMode {
DATABASE,
FILESYSTEM
}
private BinaryStorageMode binary_storage_mode = BinaryStorageMode.DATABASE;
private String binary_storage_filesystem_base_directory;
private Integer inline_resource_storage_below_size;
private Boolean bulk_export_enabled = false;
private Boolean bulk_import_enabled = false;
private Boolean default_pretty_print = true;
@@ -119,6 +127,8 @@ public class AppProperties {
private Boolean match_url_cache_enabled = false;
private Boolean index_storage_optimized = false;
private Boolean mark_resources_for_reindexing_upon_search_parameter_change = true;
private Integer reindex_thread_count = null;
private Integer expunge_thread_count = null;
public List<String> getCustomInterceptorClasses() {
return custom_interceptor_classes;
@@ -484,6 +494,22 @@ public class AppProperties {
this.binary_storage_enabled = binary_storage_enabled;
}
public BinaryStorageMode getBinary_storage_mode() {
return binary_storage_mode;
}
public void setBinary_storage_mode(BinaryStorageMode binary_storage_mode) {
this.binary_storage_mode = binary_storage_mode;
}
public String getBinary_storage_filesystem_base_directory() {
return binary_storage_filesystem_base_directory;
}
public void setBinary_storage_filesystem_base_directory(String binary_storage_filesystem_base_directory) {
this.binary_storage_filesystem_base_directory = binary_storage_filesystem_base_directory;
}
public Integer getInline_resource_storage_below_size() {
return inline_resource_storage_below_size;
}
@@ -796,6 +822,22 @@ public class AppProperties {
mark_resources_for_reindexing_upon_search_parameter_change;
}
public Integer getReindex_thread_count() {
return reindex_thread_count;
}
public void setReindex_thread_count(Integer reindex_thread_count) {
this.reindex_thread_count = reindex_thread_count;
}
public Integer getExpunge_thread_count() {
return expunge_thread_count;
}
public void setExpunge_thread_count(Integer expunge_thread_count) {
this.expunge_thread_count = expunge_thread_count;
}
public JpaStorageSettings.StoreMetaSourceInformationEnum getStore_meta_source_information() {
return store_meta_source_information;
}

View File

@@ -1,8 +1,8 @@
package ca.uhn.fhir.jpa.starter.common;
import ca.uhn.fhir.jpa.api.config.JpaStorageSettings;
import ca.uhn.fhir.jpa.binary.api.IBinaryStorageSvc;
import ca.uhn.fhir.jpa.binstore.DatabaseBinaryContentStorageSvcImpl;
import ca.uhn.fhir.jpa.binstore.FilesystemBinaryStorageSvcImpl;
import ca.uhn.fhir.jpa.config.HibernatePropertiesProvider;
import ca.uhn.fhir.jpa.model.config.PartitionSettings;
import ca.uhn.fhir.jpa.model.config.PartitionSettings.CrossPartitionReferenceMode;
@@ -19,10 +19,12 @@ import com.google.common.base.Strings;
import org.hl7.fhir.r4.model.Bundle.BundleType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.env.YamlPropertySourceLoader;
import org.springframework.context.annotation.*;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.util.Assert;
import java.util.HashSet;
import java.util.stream.Collectors;
@@ -38,6 +40,7 @@ import static org.apache.commons.lang3.ObjectUtils.defaultIfNull;
public class FhirServerConfigCommon {
private static final Logger ourLog = LoggerFactory.getLogger(FhirServerConfigCommon.class);
private static final int DEFAULT_FILESYSTEM_INLINE_THRESHOLD = 102_400;
public FhirServerConfigCommon(AppProperties appProperties) {
ourLog.info(
@@ -224,8 +227,9 @@ public class FhirServerConfigCommon {
jpaStorageSettings.setLastNEnabled(true);
}
if (appProperties.getInline_resource_storage_below_size() != 0) {
jpaStorageSettings.setInlineResourceTextBelowSize(appProperties.getInline_resource_storage_below_size());
Integer inlineResourceThreshold = resolveInlineResourceThreshold(appProperties);
if (inlineResourceThreshold != null && inlineResourceThreshold != 0) {
jpaStorageSettings.setInlineResourceTextBelowSize(inlineResourceThreshold);
}
jpaStorageSettings.setStoreResourceInHSearchIndex(appProperties.getStore_resource_in_lucene_index_enabled());
@@ -284,6 +288,21 @@ public class FhirServerConfigCommon {
jpaStorageSettings.setIndexOnContainedResources(appProperties.getEnable_index_contained_resource());
jpaStorageSettings.setIndexIdentifierOfType(appProperties.getEnable_index_of_type());
// Configure thread counts for reindex and expunge operations
if (appProperties.getReindex_thread_count() != null) {
jpaStorageSettings.setReindexThreadCount(appProperties.getReindex_thread_count());
ourLog.info(
"Server configured to use {} threads for reindex operations",
appProperties.getReindex_thread_count());
}
if (appProperties.getExpunge_thread_count() != null) {
jpaStorageSettings.setExpungeThreadCount(appProperties.getExpunge_thread_count());
ourLog.info(
"Server configured to use {} threads for expunge operations",
appProperties.getExpunge_thread_count());
}
return jpaStorageSettings;
}
@@ -341,16 +360,50 @@ public class FhirServerConfigCommon {
return new JpaHibernatePropertiesProvider(myEntityManagerFactory);
}
@Lazy
@Bean
public IBinaryStorageSvc binaryStorageSvc(AppProperties appProperties) {
DatabaseBinaryContentStorageSvcImpl binaryStorageSvc = new DatabaseBinaryContentStorageSvcImpl();
@ConditionalOnProperty(prefix = "hapi.fhir", name = "binary_storage_mode", havingValue = "FILESYSTEM")
public FilesystemBinaryStorageSvcImpl filesystemBinaryStorageSvc(AppProperties appProperties) {
String baseDirectory = appProperties.getBinary_storage_filesystem_base_directory();
Assert.hasText(
baseDirectory,
"binary_storage_filesystem_base_directory must be provided when binary_storage_mode=FILESYSTEM");
if (appProperties.getMax_binary_size() != null) {
binaryStorageSvc.setMaximumBinarySize(appProperties.getMax_binary_size());
FilesystemBinaryStorageSvcImpl filesystemSvc = new FilesystemBinaryStorageSvcImpl(baseDirectory);
Integer inlineResourceThreshold = resolveInlineResourceThreshold(appProperties);
int minimumBinarySize =
inlineResourceThreshold == null ? DEFAULT_FILESYSTEM_INLINE_THRESHOLD : inlineResourceThreshold;
filesystemSvc.setMinimumBinarySize(minimumBinarySize);
Integer maxBinarySize = appProperties.getMax_binary_size();
if (maxBinarySize != null) {
filesystemSvc.setMaximumBinarySize(maxBinarySize.longValue());
}
return binaryStorageSvc;
return filesystemSvc;
}
@Bean
@ConditionalOnProperty(
prefix = "hapi.fhir",
name = "binary_storage_mode",
havingValue = "DATABASE",
matchIfMissing = true)
public DatabaseBinaryContentStorageSvcImpl databaseBinaryStorageSvc(AppProperties appProperties) {
DatabaseBinaryContentStorageSvcImpl databaseSvc = new DatabaseBinaryContentStorageSvcImpl();
Integer maxBinarySize = appProperties.getMax_binary_size();
if (maxBinarySize != null) {
databaseSvc.setMaximumBinarySize(maxBinarySize.longValue());
}
return databaseSvc;
}
private Integer resolveInlineResourceThreshold(AppProperties appProperties) {
Integer inlineResourceThreshold = appProperties.getInline_resource_storage_below_size();
if (inlineResourceThreshold == null
&& appProperties.getBinary_storage_mode() == AppProperties.BinaryStorageMode.FILESYSTEM) {
return DEFAULT_FILESYSTEM_INLINE_THRESHOLD;
}
return inlineResourceThreshold;
}
@Bean

View File

@@ -279,6 +279,11 @@ hapi:
# filter_search_enabled: true
# graphql_enabled: true
# Thread pool configuration for maintenance operations
# Defaults to Runtime.getRuntime().availableProcessors() if not specified
# reindex_thread_count: 4 # Number of threads to use for reindex operations
# expunge_thread_count: 4 # Number of threads to use for expunge operations
# -------------------------------------------------------------------------------
# G. Narrative & Validation
# -------------------------------------------------------------------------------
@@ -381,6 +386,14 @@ hapi:
# max_page_size: 200
# retain_cached_searches_mins: 60
# reuse_cached_search_results_millis: 60000
# validation:
# requests_enabled: true
# responses_enabled: true
# binary_storage_enabled: true
# binary_storage_mode: FILESYSTEM
# binary_storage_filesystem_base_directory: /binstore
# When binary_storage_mode is FILESYSTEM and this value is not set,
# the starter defaults to 102400 bytes so smaller binaries stay inline.
inline_resource_storage_below_size: 4000
# -------------------------------------------------------------------------------

View File

@@ -0,0 +1,324 @@
package ca.uhn.fhir.jpa.starter;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.binstore.FilesystemBinaryStorageSvcImpl;
import ca.uhn.fhir.jpa.dao.data.IBinaryStorageEntityDao;
import ca.uhn.fhir.jpa.model.entity.BinaryStorageEntity;
import ca.uhn.fhir.rest.client.api.IGenericClient;
import ca.uhn.fhir.rest.client.api.ServerValidationModeEnum;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.Patient;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.support.TransactionTemplate;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ThreadLocalRandom;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
abstract class BaseBinaryStorageIntegrationTest {
protected static final String COMMON_CONFIG_LOCATION = "spring.config.location=classpath:/binary-storage-test-empty.yaml";
protected static final String COMMON_H2_USERNAME = "spring.datasource.username=sa";
protected static final String COMMON_H2_PASSWORD = "spring.datasource.password=";
protected static final String COMMON_JPA_DDL = "spring.jpa.hibernate.ddl-auto=create-drop";
protected static final String COMMON_HIBERNATE_DIALECT =
"spring.jpa.properties.hibernate.dialect=ca.uhn.fhir.jpa.model.dialect.HapiFhirH2Dialect";
protected static final String COMMON_HIBERNATE_SEARCH_DISABLED = "spring.jpa.properties.hibernate.search.enabled=false";
protected static final String COMMON_FLYWAY_DISABLED = "spring.flyway.enabled=false";
protected static final String COMMON_FHIR_VERSION = "hapi.fhir.fhir_version=r4";
protected static final String COMMON_REPO_VALIDATION_DISABLED =
"hapi.fhir.enable_repository_validating_interceptor=false";
protected static final String COMMON_MDM_DISABLED = "hapi.fhir.mdm_enabled=false";
protected static final String COMMON_CR_DISABLED = "hapi.fhir.cr_enabled=false";
protected static final String COMMON_SUBSCRIPTION_WS_DISABLED = "hapi.fhir.subscription.websocket_enabled=false";
protected static final String COMMON_BEAN_OVERRIDE_ALLOWED = "spring.main.allow-bean-definition-overriding=true";
protected static final String COMMON_CIRCULAR_REFERENCES = "spring.main.allow-circular-references=true";
protected static final String COMMON_MCP_DISABLED = "spring.ai.mcp.server.enabled=false";
protected static final String CONTENT_TYPE = "application/octet-stream";
@LocalServerPort
protected int port;
protected FhirContext fhirContext;
protected IGenericClient client;
private final List<IIdType> resourcesToDelete = new ArrayList<>();
@BeforeEach
void setUpClient() {
fhirContext = FhirContext.forR4();
fhirContext.getRestfulClientFactory().setServerValidationMode(ServerValidationModeEnum.NEVER);
fhirContext.getRestfulClientFactory().setSocketTimeout(1200 * 1000);
String serverBase = "http://localhost:" + port + "/fhir/";
client = fhirContext.newRestfulGenericClient(serverBase);
resourcesToDelete.clear();
}
@AfterEach
void deleteCreatedResources() {
for (IIdType id : resourcesToDelete) {
try {
client.delete().resourceById(id).execute();
} catch (Exception ignored) {
// Ignore cleanup failures to keep tests resilient
}
}
}
protected IIdType createPatientWithPhoto(String label, byte[] payload) {
Patient patient = new Patient();
patient.addIdentifier().setSystem("urn:binary-storage-test").setValue(label);
patient.addName().setFamily(label);
patient.addPhoto().setContentType(CONTENT_TYPE).setData(payload);
IIdType id = client.create().resource(patient).execute().getId().toUnqualifiedVersionless();
resourcesToDelete.add(id);
return id;
}
protected String uniqueLabel(String prefix) {
return prefix + "-" + UUID.randomUUID();
}
protected byte[] randomBytes(int size) {
byte[] payload = new byte[size];
ThreadLocalRandom.current().nextBytes(payload);
return payload;
}
protected void assertRegularFileCount(Path baseDir, long expectedFileCount) throws IOException {
assertThat(regularFileCount(baseDir)).isEqualTo(expectedFileCount);
}
protected void assertRegularFileCountGreaterThan(Path baseDir, long minimumFileCount) throws IOException {
assertThat(regularFileCount(baseDir)).isGreaterThan(minimumFileCount);
}
protected Path ensureDirectory(Path directory) throws IOException {
Files.createDirectories(directory);
return directory;
}
protected void deleteDirectoryContents(Path baseDir) throws IOException {
if (Files.notExists(baseDir)) {
return;
}
try (Stream<Path> files = Files.walk(baseDir)) {
files.sorted(Comparator.reverseOrder())
.filter(path -> !path.equals(baseDir))
.forEach(path -> {
try {
Files.deleteIfExists(path);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
});
}
}
private long regularFileCount(Path baseDir) throws IOException {
if (Files.notExists(baseDir)) {
return 0;
}
try (Stream<Path> files = Files.walk(baseDir)) {
return files.filter(Files::isRegularFile).count();
}
}
}
@SpringBootTest(
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
classes = Application.class,
properties = {
BaseBinaryStorageIntegrationTest.COMMON_CONFIG_LOCATION,
"spring.datasource.url=jdbc:h2:mem:binary-storage-db;DB_CLOSE_DELAY=-1",
BaseBinaryStorageIntegrationTest.COMMON_H2_USERNAME,
BaseBinaryStorageIntegrationTest.COMMON_H2_PASSWORD,
BaseBinaryStorageIntegrationTest.COMMON_JPA_DDL,
BaseBinaryStorageIntegrationTest.COMMON_HIBERNATE_DIALECT,
BaseBinaryStorageIntegrationTest.COMMON_HIBERNATE_SEARCH_DISABLED,
BaseBinaryStorageIntegrationTest.COMMON_FLYWAY_DISABLED,
BaseBinaryStorageIntegrationTest.COMMON_FHIR_VERSION,
BaseBinaryStorageIntegrationTest.COMMON_REPO_VALIDATION_DISABLED,
BaseBinaryStorageIntegrationTest.COMMON_MDM_DISABLED,
BaseBinaryStorageIntegrationTest.COMMON_CR_DISABLED,
BaseBinaryStorageIntegrationTest.COMMON_SUBSCRIPTION_WS_DISABLED,
BaseBinaryStorageIntegrationTest.COMMON_BEAN_OVERRIDE_ALLOWED,
BaseBinaryStorageIntegrationTest.COMMON_CIRCULAR_REFERENCES,
BaseBinaryStorageIntegrationTest.COMMON_MCP_DISABLED,
"hapi.fhir.binary_storage_enabled=true",
"hapi.fhir.binary_storage_mode=DATABASE"
}
)
class BinaryStorageDatabaseModeIT extends BaseBinaryStorageIntegrationTest {
@Autowired
private IBinaryStorageEntityDao binaryStorageEntityDao;
@Autowired
private PlatformTransactionManager transactionManager;
private TransactionTemplate transactionTemplate;
@BeforeEach
void initTemplate() {
transactionTemplate = new TransactionTemplate(transactionManager);
}
@Test
void largeAttachmentStoredInDatabase() {
Set<String> beforeIds = captureContentIds();
createPatientWithPhoto(uniqueLabel("database"), randomBytes(150_000));
Set<String> afterIds = captureContentIds();
afterIds.removeAll(beforeIds);
assertThat(afterIds).hasSize(1);
String binaryId = afterIds.iterator().next();
BinaryStorageEntity entity = transactionTemplate.execute(status ->
binaryStorageEntityDao.findById(binaryId).orElseThrow());
assertThat(entity.hasStorageContent()).isTrue();
assertThat(entity.getStorageContentBin()).hasSize(150_000);
transactionTemplate.execute(status -> {
binaryStorageEntityDao.deleteById(binaryId);
return null;
});
}
private Set<String> captureContentIds() {
return transactionTemplate.execute(status ->
binaryStorageEntityDao.findAll().stream()
.map(BinaryStorageEntity::getContentId)
.collect(Collectors.toCollection(LinkedHashSet::new)));
}
}
@SpringBootTest(
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
classes = Application.class,
properties = {
BaseBinaryStorageIntegrationTest.COMMON_CONFIG_LOCATION,
"spring.datasource.url=jdbc:h2:mem:binary-storage-fs-default;DB_CLOSE_DELAY=-1",
BaseBinaryStorageIntegrationTest.COMMON_H2_USERNAME,
BaseBinaryStorageIntegrationTest.COMMON_H2_PASSWORD,
BaseBinaryStorageIntegrationTest.COMMON_JPA_DDL,
BaseBinaryStorageIntegrationTest.COMMON_HIBERNATE_DIALECT,
BaseBinaryStorageIntegrationTest.COMMON_HIBERNATE_SEARCH_DISABLED,
BaseBinaryStorageIntegrationTest.COMMON_FLYWAY_DISABLED,
BaseBinaryStorageIntegrationTest.COMMON_FHIR_VERSION,
BaseBinaryStorageIntegrationTest.COMMON_REPO_VALIDATION_DISABLED,
BaseBinaryStorageIntegrationTest.COMMON_MDM_DISABLED,
BaseBinaryStorageIntegrationTest.COMMON_CR_DISABLED,
BaseBinaryStorageIntegrationTest.COMMON_SUBSCRIPTION_WS_DISABLED,
BaseBinaryStorageIntegrationTest.COMMON_BEAN_OVERRIDE_ALLOWED,
BaseBinaryStorageIntegrationTest.COMMON_CIRCULAR_REFERENCES,
BaseBinaryStorageIntegrationTest.COMMON_MCP_DISABLED,
"hapi.fhir.binary_storage_enabled=true",
"hapi.fhir.binary_storage_mode=FILESYSTEM",
"hapi.fhir.binary_storage_filesystem_base_directory=target/test-binary-storage/filesystem-default"
}
)
class BinaryStorageFilesystemDefaultIT extends BaseBinaryStorageIntegrationTest {
static final Path BASE_DIRECTORY = Paths.get("target/test-binary-storage/filesystem-default").toAbsolutePath();
@Autowired
private FilesystemBinaryStorageSvcImpl filesystemBinaryStorageSvc;
@BeforeEach
void prepareDirectory() throws IOException {
ensureDirectory(BASE_DIRECTORY);
deleteDirectoryContents(BASE_DIRECTORY);
}
@Test
void filesystemModeUsesDefaultThreshold() throws IOException {
assertThat(filesystemBinaryStorageSvc.getMinimumBinarySize()).isEqualTo(102_400);
assertRegularFileCount(BASE_DIRECTORY, 0);
createPatientWithPhoto(uniqueLabel("fs-default-inline"), randomBytes(50_000));
assertRegularFileCount(BASE_DIRECTORY, 0);
createPatientWithPhoto(uniqueLabel("fs-default-offload"), randomBytes(150_000));
assertRegularFileCountGreaterThan(BASE_DIRECTORY, 0);
}
@AfterEach
void cleanUpDirectory() throws IOException {
deleteDirectoryContents(BASE_DIRECTORY);
}
}
@SpringBootTest(
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
classes = Application.class,
properties = {
BaseBinaryStorageIntegrationTest.COMMON_CONFIG_LOCATION,
"spring.datasource.url=jdbc:h2:mem:binary-storage-fs-custom;DB_CLOSE_DELAY=-1",
BaseBinaryStorageIntegrationTest.COMMON_H2_USERNAME,
BaseBinaryStorageIntegrationTest.COMMON_H2_PASSWORD,
BaseBinaryStorageIntegrationTest.COMMON_JPA_DDL,
BaseBinaryStorageIntegrationTest.COMMON_HIBERNATE_DIALECT,
BaseBinaryStorageIntegrationTest.COMMON_HIBERNATE_SEARCH_DISABLED,
BaseBinaryStorageIntegrationTest.COMMON_FLYWAY_DISABLED,
BaseBinaryStorageIntegrationTest.COMMON_FHIR_VERSION,
BaseBinaryStorageIntegrationTest.COMMON_REPO_VALIDATION_DISABLED,
BaseBinaryStorageIntegrationTest.COMMON_MDM_DISABLED,
BaseBinaryStorageIntegrationTest.COMMON_CR_DISABLED,
BaseBinaryStorageIntegrationTest.COMMON_SUBSCRIPTION_WS_DISABLED,
BaseBinaryStorageIntegrationTest.COMMON_BEAN_OVERRIDE_ALLOWED,
BaseBinaryStorageIntegrationTest.COMMON_CIRCULAR_REFERENCES,
BaseBinaryStorageIntegrationTest.COMMON_MCP_DISABLED,
"hapi.fhir.binary_storage_enabled=true",
"hapi.fhir.binary_storage_mode=FILESYSTEM",
"hapi.fhir.binary_storage_filesystem_base_directory=target/test-binary-storage/filesystem-custom",
"hapi.fhir.inline_resource_storage_below_size=32768"
}
)
class BinaryStorageFilesystemCustomThresholdIT extends BaseBinaryStorageIntegrationTest {
static final Path BASE_DIRECTORY = Paths.get("target/test-binary-storage/filesystem-custom").toAbsolutePath();
@Autowired
private FilesystemBinaryStorageSvcImpl filesystemBinaryStorageSvc;
@BeforeEach
void prepareDirectory() throws IOException {
ensureDirectory(BASE_DIRECTORY);
deleteDirectoryContents(BASE_DIRECTORY);
}
@Test
void filesystemModeHonoursCustomThreshold() throws IOException {
assertThat(filesystemBinaryStorageSvc.getMinimumBinarySize()).isEqualTo(32_768);
assertRegularFileCount(BASE_DIRECTORY, 0);
createPatientWithPhoto(uniqueLabel("fs-custom-inline"), randomBytes(30_000));
assertRegularFileCount(BASE_DIRECTORY, 0);
createPatientWithPhoto(uniqueLabel("fs-custom-offload"), randomBytes(40_000));
assertRegularFileCountGreaterThan(BASE_DIRECTORY, 0);
}
@AfterEach
void cleanUpDirectory() throws IOException {
deleteDirectoryContents(BASE_DIRECTORY);
}
}

View File

@@ -0,0 +1,96 @@
package ca.uhn.fhir.jpa.starter.common;
import ca.uhn.fhir.jpa.binstore.DatabaseBinaryContentStorageSvcImpl;
import ca.uhn.fhir.jpa.binstore.FilesystemBinaryStorageSvcImpl;
import ca.uhn.fhir.jpa.binary.api.IBinaryStorageSvc;
import ca.uhn.fhir.jpa.starter.AppProperties;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
class FhirServerConfigCommonBinaryStorageTest {
@TempDir
Path tempDir;
private FhirServerConfigCommon newConfig() {
return new FhirServerConfigCommon(new AppProperties());
}
@Test
void defaultsToDatabaseImplementation() {
AppProperties props = new AppProperties();
IBinaryStorageSvc svc = binaryStorageSvc(props);
assertThat(svc).isInstanceOf(DatabaseBinaryContentStorageSvcImpl.class);
}
@Test
void filesystemModeUsesDefaultMinimumWhenUnspecified() throws Exception {
AppProperties props = new AppProperties();
props.setBinary_storage_mode(AppProperties.BinaryStorageMode.FILESYSTEM);
Path baseDir = tempDir.resolve("fs-default");
Files.createDirectories(baseDir);
props.setBinary_storage_filesystem_base_directory(baseDir.toString());
FilesystemBinaryStorageSvcImpl svc = filesystemBinaryStorageSvc(props);
assertThat(svc.getMinimumBinarySize()).isEqualTo(102_400);
}
@Test
void filesystemModeHonoursExplicitMinimum() throws Exception {
AppProperties props = new AppProperties();
props.setBinary_storage_mode(AppProperties.BinaryStorageMode.FILESYSTEM);
props.setInline_resource_storage_below_size(4096);
Path baseDir = tempDir.resolve("fs-min-explicit");
Files.createDirectories(baseDir);
props.setBinary_storage_filesystem_base_directory(baseDir.toString());
FilesystemBinaryStorageSvcImpl svc = filesystemBinaryStorageSvc(props);
assertThat(svc.getMinimumBinarySize()).isEqualTo(4096);
}
@Test
void filesystemModeSupportsZeroMinimumWhenExplicit() throws Exception {
AppProperties props = new AppProperties();
props.setBinary_storage_mode(AppProperties.BinaryStorageMode.FILESYSTEM);
props.setInline_resource_storage_below_size(0);
Path baseDir = tempDir.resolve("fs-zero");
Files.createDirectories(baseDir);
props.setBinary_storage_filesystem_base_directory(baseDir.toString());
FilesystemBinaryStorageSvcImpl svc = filesystemBinaryStorageSvc(props);
assertThat(svc.getMinimumBinarySize()).isZero();
}
@Test
void filesystemModeRequiresBaseDirectory() {
AppProperties props = new AppProperties();
props.setBinary_storage_mode(AppProperties.BinaryStorageMode.FILESYSTEM);
assertThatThrownBy(() -> filesystemBinaryStorageSvc(props))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("binary_storage_filesystem_base_directory");
}
private IBinaryStorageSvc binaryStorageSvc(AppProperties props) {
FhirServerConfigCommon config = newConfig();
if (props.getBinary_storage_mode() == AppProperties.BinaryStorageMode.FILESYSTEM) {
return config.filesystemBinaryStorageSvc(props);
}
return config.databaseBinaryStorageSvc(props);
}
private FilesystemBinaryStorageSvcImpl filesystemBinaryStorageSvc(AppProperties props) {
return (FilesystemBinaryStorageSvcImpl) binaryStorageSvc(props);
}
}

View File

@@ -0,0 +1 @@
# Empty config to bypass default application.yaml during BinaryStorageIntegrationTest