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}