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}