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