package org.jabref.logic.autosaveandbackup;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import org.jabref.logic.exporter.BibtexDatabaseWriter;
import org.jabref.logic.exporter.FileSaveSession;
import org.jabref.logic.exporter.SaveException;
import org.jabref.logic.exporter.SavePreferences;
import org.jabref.logic.util.io.FileUtil;
import org.jabref.model.database.BibDatabaseContext;
import org.jabref.model.database.event.BibDatabaseContextChangedEvent;
import org.jabref.preferences.JabRefPreferences;
import com.google.common.eventbus.Subscribe;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* Backups the given bib database file from {@link BibDatabaseContext} on every {@link BibDatabaseContextChangedEvent}.
* An intelligent {@link ExecutorService} with a {@link BlockingQueue} prevents a high load while making backups and
* rejects all redundant backup tasks.
* This class does not manage the .bak file which is created when opening a database.
*/
public class BackupManager {
private static final Log LOGGER = LogFactory.getLog(BackupManager.class);
private static final String BACKUP_EXTENSION = ".sav";
private static Set<BackupManager> runningInstances = new HashSet<>();
private final BibDatabaseContext bibDatabaseContext;
private final JabRefPreferences preferences;
private final ExecutorService executor;
private final Runnable backupTask = () -> determineBackupPath().ifPresent(this::performBackup);
private BackupManager(BibDatabaseContext bibDatabaseContext) {
this.bibDatabaseContext = bibDatabaseContext;
this.preferences = JabRefPreferences.getInstance();
BlockingQueue<Runnable> workerQueue = new ArrayBlockingQueue<>(1);
this.executor = new ThreadPoolExecutor(1, 1, 0, TimeUnit.SECONDS, workerQueue);
// Listen for change events
bibDatabaseContext.getDatabase().registerListener(this);
bibDatabaseContext.getMetaData().registerListener(this);
}
static Path getBackupPath(Path originalPath) {
return FileUtil.addExtension(originalPath, BACKUP_EXTENSION);
}
/**
* Starts the BackupManager which is associated with the given {@link BibDatabaseContext}.
* As long as no database file is present in {@link BibDatabaseContext}, the {@link BackupManager} will do nothing.
*
* @param bibDatabaseContext Associated {@link BibDatabaseContext}
*/
public static BackupManager start(BibDatabaseContext bibDatabaseContext) {
BackupManager backupManager = new BackupManager(bibDatabaseContext);
backupManager.startBackupTask();
runningInstances.add(backupManager);
return backupManager;
}
/**
* Shuts down the BackupManager which is associated with the given {@link BibDatabaseContext}.
*
* @param bibDatabaseContext Associated {@link BibDatabaseContext}
*/
public static void shutdown(BibDatabaseContext bibDatabaseContext) {
runningInstances.stream().filter(instance -> instance.bibDatabaseContext == bibDatabaseContext).forEach(
BackupManager::shutdown);
runningInstances.removeIf(instance -> instance.bibDatabaseContext == bibDatabaseContext);
}
/**
* Checks whether a backup file exists for the given database file.
*
* @param originalPath Path to the file a backup should be checked for.
*/
public static boolean checkForBackupFile(Path originalPath) {
Path backupPath = getBackupPath(originalPath);
return Files.exists(backupPath) && !Files.isDirectory(backupPath);
}
/**
* Restores the backup file by copying and overwriting the original one.
*
* @param originalPath Path to the file which should be equalized to the backup file.
*/
public static void restoreBackup(Path originalPath) {
Path backupPath = getBackupPath(originalPath);
try {
Files.copy(backupPath, originalPath, StandardCopyOption.REPLACE_EXISTING);
} catch (IOException e) {
LOGGER.error("Error while restoring the backup file.", e);
}
}
private Optional<Path> determineBackupPath() {
return bibDatabaseContext.getDatabasePath().map(BackupManager::getBackupPath);
}
private void performBackup(Path backupPath) {
try {
Charset charset = bibDatabaseContext.getMetaData().getEncoding().orElse(preferences.getDefaultEncoding());
SavePreferences savePreferences = SavePreferences.loadForSaveFromPreferences(preferences).withEncoding
(charset).withMakeBackup(false);
new BibtexDatabaseWriter<>(FileSaveSession::new).saveDatabase(bibDatabaseContext, savePreferences).commit
(backupPath);
} catch (SaveException e) {
LOGGER.error("Error while saving file.", e);
}
}
@Subscribe
public synchronized void listen(@SuppressWarnings("unused") BibDatabaseContextChangedEvent event) {
startBackupTask();
}
private void startBackupTask() {
try {
executor.submit(backupTask);
} catch (RejectedExecutionException e) {
LOGGER.debug("Rejecting while another backup process is already running.");
}
}
/**
* Unregisters the BackupManager from the eventBus of {@link BibDatabaseContext} and deletes the backup file.
* This method should only be used when closing a database/JabRef legally.
*/
private void shutdown() {
bibDatabaseContext.getDatabase().unregisterListener(this);
bibDatabaseContext.getMetaData().unregisterListener(this);
executor.shutdown();
determineBackupPath().ifPresent(this::deleteBackupFile);
}
private void deleteBackupFile(Path backupPath) {
try {
if (Files.exists(backupPath) && !Files.isDirectory(backupPath)) {
Files.delete(backupPath);
}
} catch (IOException e) {
LOGGER.error("Error while deleting the backup file.", e);
}
}
}