<commit_before>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.model.entry.event.FieldChangedEvent;
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 final int MINOR_CHANGES_LIMIT = 5;

    private static Set<BackupManager> runningInstances = new HashSet<>();

    private int minorChangesCount = 0;

    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) {
        if (!(event instanceof FieldChangedEvent)) {
            startBackupTask();
        } else {
            // only do a backup if the field changes are more than one character or if there are enough of them
            FieldChangedEvent fieldChange = (FieldChangedEvent) event;
            if (fieldChange.getDelta() > 1 || minorChangesCount >= MINOR_CHANGES_LIMIT) {
                startBackupTask();
                minorChangesCount = 0;
            } else {
                minorChangesCount++;
            }
        }
    }

    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) {<commit_msg>Only do a back up for bigger changes or if a different field is edited (#3004)<commit_after>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.model.entry.event.FieldChangedEvent;
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 String lastFieldChanged;

    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) {
        if (!(event instanceof FieldChangedEvent)) {
            startBackupTask();
        } else {
            // only do a backup if the field changes are more than one character or a new field is edited
            FieldChangedEvent fieldChange = (FieldChangedEvent) event;
            boolean isEditOnNewField = lastFieldChanged == null || !lastFieldChanged.equals(fieldChange.getFieldName());

            if (fieldChange.getDelta() > 1 || isEditOnNewField) {
                lastFieldChanged = fieldChange.getFieldName();
                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) {
{'fraction_non_alphanumeric': 0.08524067138299347, 'fraction_numerical': 0.0011790817034262728, 'mean_word_length': 3.405438435685915, 'pattern_counts': {'":': 0, '<': 15, '<?xml version=': 0, '>': 24, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '6816759', 'n_tokens_mistral': 3776, 'n_tokens_neox': 3635, 'n_words': 957}