001/*
002 * Licensed to DuraSpace under one or more contributor license agreements.
003 * See the NOTICE file distributed with this work for additional information
004 * regarding copyright ownership.
005 *
006 * DuraSpace licenses this file to you under the Apache License,
007 * Version 2.0 (the "License"); you may not use this file except in
008 * compliance with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018
019package org.fcrepo.config;
020
021import java.io.IOException;
022import java.nio.file.Files;
023import java.nio.file.Path;
024import java.util.List;
025import java.util.Objects;
026import java.util.stream.Collectors;
027
028import javax.annotation.PostConstruct;
029
030import org.slf4j.Logger;
031import org.slf4j.LoggerFactory;
032import org.springframework.beans.factory.annotation.Value;
033import org.springframework.context.annotation.Configuration;
034
035/**
036 * Fedora's OCFL related configuration properties
037 *
038 * @author pwinckles
039 * @since 6.0.0
040 */
041@Configuration
042public class OcflPropsConfig extends BasePropsConfig {
043
044    private static final Logger LOGGER = LoggerFactory.getLogger(OcflPropsConfig.class);
045
046    public static final String FCREPO_OCFL_STAGING = "fcrepo.ocfl.staging";
047    public static final String FCREPO_OCFL_ROOT = "fcrepo.ocfl.root";
048    public static final String FCREPO_OCFL_TEMP = "fcrepo.ocfl.temp";
049    private static final String FCREPO_OCFL_S3_BUCKET = "fcrepo.ocfl.s3.bucket";
050
051    private static final String OCFL_STAGING = "staging";
052    private static final String OCFL_ROOT = "ocfl-root";
053    private static final String OCFL_TEMP = "ocfl-temp";
054
055    private static final String FCREPO_PERSISTENCE_ALGORITHM = "fcrepo.persistence.defaultDigestAlgorithm";
056
057    @Value("${" + FCREPO_OCFL_STAGING + ":#{fedoraPropsConfig.fedoraData.resolve('" + OCFL_STAGING + "')}}")
058    private Path fedoraOcflStaging;
059
060    @Value("${" + FCREPO_OCFL_ROOT + ":#{fedoraPropsConfig.fedoraData.resolve('" + OCFL_ROOT + "')}}")
061    private Path ocflRepoRoot;
062
063    @Value("${" + FCREPO_OCFL_TEMP + ":#{fedoraPropsConfig.fedoraData.resolve('" + OCFL_TEMP + "')}}")
064    private Path ocflTemp;
065
066    /**
067     * Controls whether changes are committed to new OCFL versions or to a mutable HEAD
068     */
069    @Value("${fcrepo.autoversioning.enabled:true}")
070    private boolean autoVersioningEnabled;
071
072    @Value("${fcrepo.storage:ocfl-fs}")
073    private String storageStr;
074    private Storage storage;
075
076    @Value("${fcrepo.aws.access-key:}")
077    private String awsAccessKey;
078
079    @Value("${fcrepo.aws.secret-key:}")
080    private String awsSecretKey;
081
082    @Value("${fcrepo.aws.region:}")
083    private String awsRegion;
084
085    @Value("${" + FCREPO_OCFL_S3_BUCKET + ":}")
086    private String ocflS3Bucket;
087
088    @Value("${fcrepo.ocfl.s3.prefix:}")
089    private String ocflS3Prefix;
090
091    @Value("${fcrepo.resource-header-cache.enable:true}")
092    private boolean resourceHeadersCacheEnabled;
093
094    @Value("${fcrepo.resource-header-cache.max-size:512}")
095    private long resourceHeadersCacheMaxSize;
096
097    @Value("${fcrepo.resource-header-cache.expire-after-seconds:600}")
098    private long resourceHeadersCacheExpireAfterSeconds;
099
100    @Value("${fcrepo.ocfl.reindex.threads:-1}")
101    private long reindexThreads;
102
103    @Value("${fcrepo.ocfl.reindex.batchSize:100}")
104    private long reindexBatchSize;
105
106    @Value("${fcrepo.ocfl.reindex.failOnError:true}")
107    private boolean reindexFailOnError;
108
109    @Value("${" + FCREPO_PERSISTENCE_ALGORITHM + ":sha512}")
110    private String FCREPO_DIGEST_ALGORITHM_VALUE;
111
112    private DigestAlgorithm FCREPO_DIGEST_ALGORITHM;
113
114    /**
115     * List of valid choices for fcrepo.persistence.defaultDigestAlgorithm
116     */
117    private static final List<DigestAlgorithm> FCREPO_VALID_DIGEST_ALGORITHMS = List.of(
118            DigestAlgorithm.SHA256,
119            DigestAlgorithm.SHA512
120    );
121
122    private static final long availableThreads = Runtime.getRuntime().availableProcessors();
123
124    @PostConstruct
125    private void postConstruct() throws IOException {
126        if (reindexThreads < 0L) {
127            reindexThreads = computeDefaultReindexThreads();
128        } else {
129            reindexThreads = checkReindexThreadLimit(reindexThreads);
130        }
131        storage = Storage.fromString(storageStr);
132        LOGGER.info("Fedora storage type: {}", storage);
133        LOGGER.info("Fedora staging: {}", fedoraOcflStaging);
134        LOGGER.info("Fedora OCFL temp: {}", ocflTemp);
135        LOGGER.info("Fedora OCFL reindexing threads: {}", reindexThreads);
136        LOGGER.info("Fedora OCFL reindexing batch size: {}", reindexBatchSize);
137        LOGGER.info("Fedora OCFL reindexing fail on error: {}", reindexFailOnError);
138        Files.createDirectories(fedoraOcflStaging);
139        Files.createDirectories(ocflTemp);
140
141        if (storage == Storage.OCFL_FILESYSTEM) {
142            LOGGER.info("Fedora OCFL root: {}", ocflRepoRoot);
143            Files.createDirectories(ocflRepoRoot);
144        } else if (storage == Storage.OCFL_S3) {
145            Objects.requireNonNull(ocflS3Bucket,
146                    String.format("The property %s must be set when OCFL S3 storage is used", FCREPO_OCFL_S3_BUCKET));
147
148            LOGGER.info("Fedora AWS access key: {}", awsAccessKey);
149            LOGGER.info("Fedora AWS secret key set: {}", Objects.isNull(awsSecretKey));
150            LOGGER.info("Fedora AWS region: {}", awsRegion);
151            LOGGER.info("Fedora OCFL S3 bucket: {}", ocflS3Bucket);
152            LOGGER.info("Fedora OCFL S3 prefix: {}", ocflS3Prefix);
153        }
154        FCREPO_DIGEST_ALGORITHM = DigestAlgorithm.fromAlgorithm(FCREPO_DIGEST_ALGORITHM_VALUE);
155        // Throw error if the configured default digest is not known to fedora or is not a valid option
156        if (DigestAlgorithm.MISSING.equals(FCREPO_DIGEST_ALGORITHM) ||
157                !FCREPO_VALID_DIGEST_ALGORITHMS.contains(FCREPO_DIGEST_ALGORITHM)) {
158            throw new IllegalArgumentException(String.format("Invalid %s property configured: %s, must be one of %s",
159                    FCREPO_PERSISTENCE_ALGORITHM, FCREPO_DIGEST_ALGORITHM_VALUE,
160                    FCREPO_VALID_DIGEST_ALGORITHMS.stream().map(DigestAlgorithm::getAlgorithm)
161                            .collect(Collectors.joining(", "))));
162        }
163        LOGGER.info("Fedora OCFL digest algorithm: {}", FCREPO_DIGEST_ALGORITHM.getAlgorithm());
164    }
165
166    /**
167     * @return Path to directory Fedora stages resources before moving them into OCFL
168     */
169    public Path getFedoraOcflStaging() {
170        return fedoraOcflStaging;
171    }
172
173    /**
174     * Sets the path to the Fedora staging directory -- should only be used for testing purposes.
175     *
176     * @param fedoraOcflStaging Path to Fedora staging directory
177     */
178    public void setFedoraOcflStaging(final Path fedoraOcflStaging) {
179        this.fedoraOcflStaging = fedoraOcflStaging;
180    }
181
182    /**
183     * @return Path to OCFL root directory
184     */
185    public Path getOcflRepoRoot() {
186        return ocflRepoRoot;
187    }
188
189    /**
190     * Sets the path to the Fedora OCFL root directory -- should only be used for testing purposes.
191     *
192     * @param ocflRepoRoot Path to Fedora OCFL root directory
193     */
194    public void setOcflRepoRoot(final Path ocflRepoRoot) {
195        this.ocflRepoRoot = ocflRepoRoot;
196    }
197
198    /**
199     * @return Path to the temp directory used by the OCFL client
200     */
201    public Path getOcflTemp() {
202        return ocflTemp;
203    }
204
205    /**
206     * Sets the path to the OCFL temp directory -- should only be used for testing purposes.
207     *
208     * @param ocflTemp Path to OCFL temp directory
209     */
210    public void setOcflTemp(final Path ocflTemp) {
211        this.ocflTemp = ocflTemp;
212    }
213
214    /**
215     * @return true if every update should create a new OCFL version; false if the mutable HEAD should be used
216     */
217    public boolean isAutoVersioningEnabled() {
218        return autoVersioningEnabled;
219    }
220
221    /**
222     * Determines whether or not new OCFL versions are created on every update.
223     *
224     * @param autoVersioningEnabled true to create new versions on every update
225     */
226    public void setAutoVersioningEnabled(final boolean autoVersioningEnabled) {
227        this.autoVersioningEnabled = autoVersioningEnabled;
228    }
229
230    /**
231     * @return Indicates the storage type. ocfl-fs is the default
232     */
233    public Storage getStorage() {
234        return storage;
235    }
236
237    /**
238     * @param storage storage to use
239     */
240    public void setStorage(final Storage storage) {
241        this.storage = storage;
242    }
243
244    /**
245     * @return the aws access key to use, may be null
246     */
247    public String getAwsAccessKey() {
248        return awsAccessKey;
249    }
250
251    /**
252     * @param awsAccessKey the aws access key to use
253     */
254    public void setAwsAccessKey(final String awsAccessKey) {
255        this.awsAccessKey = awsAccessKey;
256    }
257
258    /**
259     * @return the aws secret key to use, may be null
260     */
261    public String getAwsSecretKey() {
262        return awsSecretKey;
263    }
264
265    /**
266     * @param awsSecretKey the aws secret key to use
267     */
268    public void setAwsSecretKey(final String awsSecretKey) {
269        this.awsSecretKey = awsSecretKey;
270    }
271
272    /**
273     * @return the aws region to use, may be null
274     */
275    public String getAwsRegion() {
276        return awsRegion;
277    }
278
279    /**
280     * @param awsRegion the aws region to use
281     */
282    public void setAwsRegion(final String awsRegion) {
283        this.awsRegion = awsRegion;
284    }
285
286    /**
287     * @return the s3 bucket to store objects in
288     */
289    public String getOcflS3Bucket() {
290        return ocflS3Bucket;
291    }
292
293    /**
294     * @param ocflS3Bucket sets the s3 bucket to store objects in
295     */
296    public void setOcflS3Bucket(final String ocflS3Bucket) {
297        this.ocflS3Bucket = ocflS3Bucket;
298    }
299
300    /**
301     * @return the s3 prefix to store objects under, may be null
302     */
303    public String getOcflS3Prefix() {
304        return ocflS3Prefix;
305    }
306
307    /**
308     * @param ocflS3Prefix the prefix to store objects under
309     */
310    public void setOcflS3Prefix(final String ocflS3Prefix) {
311        this.ocflS3Prefix = ocflS3Prefix;
312    }
313
314    /**
315     * @return whether or not to enable the resource headers cache
316     */
317    public boolean isResourceHeadersCacheEnabled() {
318        return resourceHeadersCacheEnabled;
319    }
320
321    /**
322     * @param resourceHeadersCacheEnabled whether or not to enable the resource headers cache
323     */
324    public void setResourceHeadersCacheEnabled(final boolean resourceHeadersCacheEnabled) {
325        this.resourceHeadersCacheEnabled = resourceHeadersCacheEnabled;
326    }
327
328    /**
329     * @return maximum number or resource headers in cache
330     */
331    public long getResourceHeadersCacheMaxSize() {
332        return resourceHeadersCacheMaxSize;
333    }
334
335    /**
336     * @param resourceHeadersCacheMaxSize maximum number of resource headers in cache
337     */
338    public void setResourceHeadersCacheMaxSize(final long resourceHeadersCacheMaxSize) {
339        this.resourceHeadersCacheMaxSize = resourceHeadersCacheMaxSize;
340    }
341
342    /**
343     * @return number of seconds to wait before expiring a resource header from the cache
344     */
345    public long getResourceHeadersCacheExpireAfterSeconds() {
346        return resourceHeadersCacheExpireAfterSeconds;
347    }
348
349    /**
350     * @param resourceHeadersCacheExpireAfterSeconds
351     *      number of seconds to wait before expiring a resource header from the cache
352     */
353    public void setResourceHeadersCacheExpireAfterSeconds(final long resourceHeadersCacheExpireAfterSeconds) {
354        this.resourceHeadersCacheExpireAfterSeconds = resourceHeadersCacheExpireAfterSeconds;
355    }
356
357    /**
358     * @param threads
359     *   number of threads to use when rebuilding from Fedora OCFL on disk.
360     */
361    public void setReindexingThreads(final long threads) {
362        this.reindexThreads = checkReindexThreadLimit(threads);
363    }
364
365    /**
366     * @return number of threads to use when rebuilding from Fedora OCFL on disk.
367     */
368    public long getReindexingThreads() {
369        return this.reindexThreads;
370    }
371
372    /**
373     * @return number of OCFL ids for a the reindexing manager to hand out at once.
374     */
375    public long getReindexBatchSize() {
376        return reindexBatchSize;
377    }
378
379    /**
380     * @param reindexBatchSize
381     *   number of OCFL ids for a the reindexing manager to hand out at once.
382     */
383    public void setReindexBatchSize(final long reindexBatchSize) {
384        this.reindexBatchSize = reindexBatchSize;
385    }
386
387    /**
388     * @return whether to stop the entire reindexing process if a single object fails.
389     */
390    public boolean isReindexFailOnError() {
391        return reindexFailOnError;
392    }
393
394    /**
395     * @param reindexFailOnError
396     *   whether to stop the entire reindexing process if a single object fails.
397     */
398    public void setReindexFailOnError(final boolean reindexFailOnError) {
399        this.reindexFailOnError = reindexFailOnError;
400    }
401
402    /**
403     * Check we don't create too few reindexing threads.
404     * @param threads the number of threads requested.
405     * @return higher of the requested amount or 1
406     */
407    private long checkReindexThreadLimit(final long threads) {
408       if (threads <= 0) {
409            LOGGER.warn("Can't have fewer than 1 reindexing thread, setting to 1.");
410            return 1;
411        } else {
412            return threads;
413        }
414    }
415
416    /**
417     * @return number of available processors minus 1.
418     */
419    private static long computeDefaultReindexThreads() {
420        return availableThreads - 1;
421    }
422
423    /**
424     * @return the configured OCFL digest algorithm
425     */
426    public DigestAlgorithm getDefaultDigestAlgorithm() {
427        return FCREPO_DIGEST_ALGORITHM;
428    }
429}