001/* 002 * The contents of this file are subject to the license and copyright 003 * detailed in the LICENSE and NOTICE files at the root of the source 004 * tree. 005 */ 006package org.fcrepo.persistence.ocfl.impl; 007 008import static org.fcrepo.persistence.ocfl.impl.OcflPersistentStorageUtils.createFilesystemRepository; 009import static org.fcrepo.persistence.ocfl.impl.OcflPersistentStorageUtils.createS3Repository; 010 011import java.io.IOException; 012import java.net.URI; 013import java.time.Duration; 014import java.util.concurrent.TimeUnit; 015 016import javax.inject.Inject; 017import javax.sql.DataSource; 018 019import org.fcrepo.config.MetricsConfig; 020import org.fcrepo.config.OcflPropsConfig; 021import org.fcrepo.config.Storage; 022import org.fcrepo.storage.ocfl.CommitType; 023import org.fcrepo.storage.ocfl.DefaultOcflObjectSessionFactory; 024import org.fcrepo.storage.ocfl.validation.ObjectValidator; 025import org.fcrepo.storage.ocfl.OcflObjectSessionFactory; 026import org.fcrepo.storage.ocfl.ResourceHeaders; 027import org.fcrepo.storage.ocfl.cache.Cache; 028import org.fcrepo.storage.ocfl.cache.CaffeineCache; 029import org.fcrepo.storage.ocfl.cache.NoOpCache; 030 031import org.apache.commons.lang3.StringUtils; 032import org.springframework.context.annotation.Bean; 033import org.springframework.context.annotation.Configuration; 034 035import com.github.benmanes.caffeine.cache.Caffeine; 036 037import io.ocfl.api.MutableOcflRepository; 038import io.micrometer.core.instrument.MeterRegistry; 039import io.micrometer.core.instrument.binder.cache.CaffeineCacheMetrics; 040import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; 041import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; 042import software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClient; 043import software.amazon.awssdk.regions.Region; 044import software.amazon.awssdk.services.s3.S3AsyncClient; 045 046/** 047 * A Configuration for OCFL dependencies 048 * 049 * @author dbernstein 050 * @since 6.0.0 051 */ 052@Configuration 053public class OcflPersistenceConfig { 054 055 @Inject 056 private OcflPropsConfig ocflPropsConfig; 057 058 @Inject 059 private MetricsConfig metricsConfig; 060 061 @Inject 062 private MeterRegistry meterRegistry; 063 064 @Inject 065 private DataSource dataSource; 066 067 /** 068 * Create an OCFL Repository 069 * @return the repository 070 */ 071 @Bean 072 public MutableOcflRepository repository() throws IOException { 073 if (ocflPropsConfig.getStorage() == Storage.OCFL_S3) { 074 return createS3Repository( 075 dataSource, 076 s3Client(), 077 s3CrtClient(), 078 ocflPropsConfig.getOcflS3Bucket(), 079 ocflPropsConfig.getOcflS3Prefix(), 080 ocflPropsConfig.getOcflTemp(), 081 ocflPropsConfig.getDefaultDigestAlgorithm(), 082 ocflPropsConfig.isOcflS3DbEnabled(), 083 ocflPropsConfig.isOcflUpgradeOnWrite(), 084 ocflPropsConfig.verifyInventory()); 085 } else { 086 return createFilesystemRepository(ocflPropsConfig.getOcflRepoRoot(), ocflPropsConfig.getOcflTemp(), 087 ocflPropsConfig.getDefaultDigestAlgorithm(), ocflPropsConfig.isOcflUpgradeOnWrite(), 088 ocflPropsConfig.verifyInventory()); 089 } 090 } 091 092 @Bean 093 public OcflObjectSessionFactory ocflObjectSessionFactory() throws IOException { 094 final var objectMapper = OcflPersistentStorageUtils.objectMapper(); 095 096 final var factory = new DefaultOcflObjectSessionFactory(repository(), 097 ocflPropsConfig.getFedoraOcflStaging(), 098 objectMapper, 099 createCache("resourceHeadersCache"), 100 createCache("rootIdCache"), 101 commitType(), 102 "Authored by Fedora 6", 103 "fedoraAdmin", 104 "info:fedora/fedoraAdmin"); 105 factory.useUnsafeWrite(ocflPropsConfig.isUnsafeWriteEnabled()); 106 return factory; 107 } 108 109 @Bean 110 public ObjectValidator objectValidator() throws IOException { 111 final var objectMapper = OcflPersistentStorageUtils.objectMapper(); 112 return new ObjectValidator(repository(), objectMapper.readerFor(ResourceHeaders.class)); 113 } 114 115 private CommitType commitType() { 116 if (ocflPropsConfig.isAutoVersioningEnabled()) { 117 return CommitType.NEW_VERSION; 118 } 119 return CommitType.UNVERSIONED; 120 } 121 122 private S3AsyncClient s3CrtClient() { 123 final var builder = S3AsyncClient.crtBuilder() 124 .checksumValidationEnabled(ocflPropsConfig.isOcflS3ChecksumEnabled()); 125 126 if (StringUtils.isNotBlank(ocflPropsConfig.getAwsRegion())) { 127 builder.region(Region.of(ocflPropsConfig.getAwsRegion())); 128 } 129 130 if (StringUtils.isNotBlank(ocflPropsConfig.getS3Endpoint())) { 131 builder.endpointOverride(URI.create(ocflPropsConfig.getS3Endpoint())); 132 } 133 134 if (StringUtils.isNoneBlank(ocflPropsConfig.getAwsAccessKey(), ocflPropsConfig.getAwsSecretKey())) { 135 builder.credentialsProvider(StaticCredentialsProvider.create( 136 AwsBasicCredentials.create(ocflPropsConfig.getAwsAccessKey(), ocflPropsConfig.getAwsSecretKey()))); 137 } 138 139 return builder.build(); 140 } 141 142 private S3AsyncClient s3Client() { 143 final var builder = S3AsyncClient.builder(); 144 145 if (StringUtils.isNotBlank(ocflPropsConfig.getAwsRegion())) { 146 builder.region(Region.of(ocflPropsConfig.getAwsRegion())); 147 } 148 149 if (StringUtils.isNotBlank(ocflPropsConfig.getS3Endpoint())) { 150 builder.endpointOverride(URI.create(ocflPropsConfig.getS3Endpoint())); 151 } 152 153 if (ocflPropsConfig.isPathStyleAccessEnabled()) { 154 builder.serviceConfiguration(config -> config.pathStyleAccessEnabled(true)); 155 } 156 157 if (StringUtils.isNoneBlank(ocflPropsConfig.getAwsAccessKey(), ocflPropsConfig.getAwsSecretKey())) { 158 builder.credentialsProvider(StaticCredentialsProvider.create( 159 AwsBasicCredentials.create(ocflPropsConfig.getAwsAccessKey(), ocflPropsConfig.getAwsSecretKey()))); 160 } 161 162 // May want to do additional HTTP client configuration, connection pool, etc 163 final var httpClientBuilder = NettyNioAsyncHttpClient.builder() 164 .connectionAcquisitionTimeout(Duration.ofSeconds(ocflPropsConfig.getS3ConnectionTimeout())) 165 .writeTimeout(Duration.ofSeconds(ocflPropsConfig.getS3WriteTimeout())) 166 .readTimeout(Duration.ofSeconds(ocflPropsConfig.getS3ReadTimeout())) 167 .maxConcurrency(ocflPropsConfig.getS3MaxConcurrency()); 168 builder.httpClientBuilder(httpClientBuilder); 169 170 return builder.build(); 171 } 172 173 private <K, V> Cache<K, V> createCache(final String metricName) { 174 if (ocflPropsConfig.isResourceHeadersCacheEnabled()) { 175 final var builder = Caffeine.newBuilder(); 176 177 if (metricsConfig.isMetricsEnabled()) { 178 builder.recordStats(); 179 } 180 181 final var cache = builder 182 .maximumSize(ocflPropsConfig.getResourceHeadersCacheMaxSize()) 183 .expireAfterAccess(ocflPropsConfig.getResourceHeadersCacheExpireAfterSeconds(), TimeUnit.SECONDS) 184 .build(); 185 186 if (metricsConfig.isMetricsEnabled()) { 187 CaffeineCacheMetrics.monitor(meterRegistry, cache, metricName); 188 } 189 190 return new CaffeineCache<>(cache); 191 } 192 193 return new NoOpCache<>(); 194 } 195 196}