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 io.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        final String logMessage;
085        if (fedoraPropsConfig.isRebuildContinue()) {
086            logMessage = "Initiating partial index rebuild. This will add missing objects to the index.";
087        } else {
088            logMessage = "Initiating index rebuild.";
089        }
090        LOGGER.info(logMessage + " This may take a while. Progress will be logged periodically.");
091
092        if (!fedoraPropsConfig.isRebuildContinue()) {
093            LOGGER.debug("Clearing all indexes");
094            reindexService.reset();
095        }
096
097        try (var objectIds = ocflRepository.listObjectIds()) {
098            final ReindexManager reindexManager = new ReindexManager(objectIds,
099                    reindexService, ocflPropsConfig, txManager, dbTransactionExecutor);
100
101            LOGGER.debug("Reading object ids...");
102            final var startTime = Instant.now();
103            try {
104                reindexManager.start();
105            } catch (final InterruptedException e) {
106                throw new RuntimeException(e);
107            } finally {
108                reindexManager.shutdown();
109            }
110            final var endTime = Instant.now();
111            final var count = reindexManager.getCompletedCount();
112            final var errors = reindexManager.getErrorCount();
113            final var skipped = reindexManager.getSkippedCount();
114            if (fedoraPropsConfig.isRebuildContinue()) {
115                LOGGER.info(
116                    "Index rebuild completed {} objects successfully, {} objects skipped and {} objects had errors " +
117                    "in {} ", count, skipped, errors, getDurationMessage(Duration.between(startTime, endTime))
118                );
119            } else {
120                LOGGER.info(
121                    "Index rebuild completed {} objects successfully and {} objects had errors in {} ",
122                    count, errors, getDurationMessage(Duration.between(startTime, endTime))
123                );
124            }
125        }
126    }
127
128    private boolean shouldRebuild() {
129        final var repoRoot = getRepoRootMapping();
130        if (fedoraPropsConfig.isRebuildOnStart() || fedoraPropsConfig.isRebuildContinue()) {
131            return true;
132        } else if (repoRoot == null) {
133            return true;
134        } else {
135            return !repoContainsRootObject(repoRoot);
136        }
137    }
138
139    private String getRepoRootMapping() {
140        try {
141            return ocflIndex.getMapping(ReadOnlyTransaction.INSTANCE, FedoraId.getRepositoryRootId()).getOcflObjectId();
142        } catch (final FedoraOcflMappingNotFoundException e) {
143            return null;
144        }
145    }
146
147    private boolean repoContainsRootObject(final String id) {
148        return ocflRepository.containsObject(id);
149    }
150
151    private String getDurationMessage(final Duration duration) {
152        String message = String.format("%d seconds", duration.toSecondsPart());
153        if (duration.getSeconds() > 60) {
154            message = String.format("%d mins, ", duration.toMinutesPart()) + message;
155        }
156        if (duration.getSeconds() > 3600) {
157            message = String.format("%d hours, ", duration.toHoursPart()) + message;
158        }
159        return message;
160    }
161}