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 edu.wisc.library.ocfl.api.OcflRepository;
009
010import org.fcrepo.common.db.DbTransactionExecutor;
011import org.fcrepo.config.FedoraPropsConfig;
012import org.fcrepo.config.OcflPropsConfig;
013import org.fcrepo.kernel.api.ContainmentIndex;
014import org.fcrepo.kernel.api.ReadOnlyTransaction;
015import org.fcrepo.kernel.api.TransactionManager;
016import org.fcrepo.kernel.api.identifiers.FedoraId;
017import org.fcrepo.persistence.ocfl.api.FedoraOcflMappingNotFoundException;
018import org.fcrepo.persistence.ocfl.api.FedoraToOcflObjectIndex;
019import org.fcrepo.persistence.ocfl.api.IndexBuilder;
020import org.slf4j.Logger;
021import org.slf4j.LoggerFactory;
022import org.springframework.beans.factory.annotation.Autowired;
023import org.springframework.beans.factory.annotation.Qualifier;
024import org.springframework.stereotype.Component;
025
026import javax.inject.Inject;
027import java.time.Duration;
028import java.time.Instant;
029
030/**
031 * An implementation of {@link IndexBuilder}.  This implementation rebuilds the following indexable state derived
032 * from the underlying OCFL directory:
033 * 1) the link between a {@link org.fcrepo.kernel.api.identifiers.FedoraId} and an OCFL object identifier
034 * 2) the containment relationships between {@link org.fcrepo.kernel.api.identifiers.FedoraId}s
035 * 3) the reference relationships between {@link org.fcrepo.kernel.api.identifiers.FedoraId}s
036 * 4) the search index
037 * 5) the membership relationships for Direct and Indirect containers.
038 *
039 * @author dbernstein
040 * @author whikloj
041 * @since 6.0.0
042 */
043@Component
044public class IndexBuilderImpl implements IndexBuilder {
045
046    private static final Logger LOGGER = LoggerFactory.getLogger(IndexBuilderImpl.class);
047
048    @Autowired
049    @Qualifier("ocflIndex")
050    private FedoraToOcflObjectIndex ocflIndex;
051
052    @Autowired
053    @Qualifier("containmentIndex")
054    private ContainmentIndex containmentIndex;
055
056    @Inject
057    private OcflRepository ocflRepository;
058
059    @Inject
060    private ReindexService reindexService;
061
062    @Inject
063    private OcflPropsConfig ocflPropsConfig;
064
065    @Inject
066    private FedoraPropsConfig fedoraPropsConfig;
067
068    @Inject
069    private TransactionManager txManager;
070
071    @Inject
072    private DbTransactionExecutor dbTransactionExecutor;
073
074    @Override
075    public void rebuildIfNecessary() {
076        if (shouldRebuild()) {
077            rebuild();
078        } else {
079            LOGGER.debug("No index rebuild necessary");
080        }
081    }
082
083    private void rebuild() {
084        LOGGER.info("Initiating index rebuild. This may take a while. Progress will be logged periodically.");
085
086        reindexService.reset();
087
088        try (var objectIds = ocflRepository.listObjectIds()) {
089            final ReindexManager reindexManager = new ReindexManager(objectIds,
090                    reindexService, ocflPropsConfig, txManager, dbTransactionExecutor);
091
092            LOGGER.debug("Reading object ids...");
093            final var startTime = Instant.now();
094            try {
095                reindexManager.start();
096            } catch (final InterruptedException e) {
097                throw new RuntimeException(e);
098            } finally {
099                reindexManager.shutdown();
100            }
101            final var endTime = Instant.now();
102            final var count = reindexManager.getCompletedCount();
103            final var errors = reindexManager.getErrorCount();
104            LOGGER.info("Index rebuild completed {} objects successfully and {} objects had errors in {} ",
105                    count, errors, getDurationMessage(Duration.between(startTime, endTime)));
106        }
107    }
108
109    private boolean shouldRebuild() {
110        final var repoRoot = getRepoRootMapping();
111        if (fedoraPropsConfig.isRebuildOnStart()) {
112            return true;
113        } else if (repoRoot == null) {
114            return true;
115        } else {
116            return !repoContainsRootObject(repoRoot);
117        }
118    }
119
120    private String getRepoRootMapping() {
121        try {
122            return ocflIndex.getMapping(ReadOnlyTransaction.INSTANCE, FedoraId.getRepositoryRootId()).getOcflObjectId();
123        } catch (final FedoraOcflMappingNotFoundException e) {
124            return null;
125        }
126    }
127
128    private boolean repoContainsRootObject(final String id) {
129        return ocflRepository.containsObject(id);
130    }
131
132    private String getDurationMessage(final Duration duration) {
133        String message = String.format("%d seconds", duration.toSecondsPart());
134        if (duration.getSeconds() > 60) {
135            message = String.format("%d mins, ", duration.toMinutesPart()) + message;
136        }
137        if (duration.getSeconds() > 3600) {
138            message = String.format("%d hours, ", duration.getSeconds() / 3600) + message;
139        }
140        return message;
141    }
142}