From 3f60ff328d885ca89033de42869494d05c98ea14 Mon Sep 17 00:00:00 2001
From: Adham Ahmed Hussein Mahrous <adhamahmad541@gmail.com>
Date: Sat, 22 Mar 2025 21:54:04 +0200
Subject: [PATCH 1/2] Merge branch 'fix-for-issue-10557' of
 https://github.com/adhamahmad/jabref into fix-for-issue-10557

---
 .../journals/AbbreviationsFileViewModel.java  |  32 +++-
 .../journals/JournalAbbreviationsTab.fxml     |   5 +
 .../journals/JournalAbbreviationsTab.java     |   9 ++
 .../JournalAbbreviationsTabViewModel.java     |  72 ++++++++-
 .../journals/JournalAbbreviationLoader.java   |  22 ++-
 .../JournalAbbreviationMvGenerator.java       | 149 ++++++++++++++++++
 .../JournalAbbreviationPreferences.java       |  92 ++++++++++-
 .../JournalAbbreviationRepository.java        |   6 +-
 .../preferences/JabRefCliPreferences.java     |  16 +-
 .../org/jabref/logic/util/Directories.java    |   7 +
 .../JournalAbbreviationMvGeneratorTest.java   |  81 ++++++++++
 11 files changed, 476 insertions(+), 15 deletions(-)
 create mode 100644 src/main/java/org/jabref/logic/journals/JournalAbbreviationMvGenerator.java
 create mode 100644 src/test/java/org/jabref/logic/journals/JournalAbbreviationMvGeneratorTest.java

diff --git a/src/main/java/org/jabref/gui/preferences/journals/AbbreviationsFileViewModel.java b/src/main/java/org/jabref/gui/preferences/journals/AbbreviationsFileViewModel.java
index 88ecb96d240..b22dd902d95 100644
--- a/src/main/java/org/jabref/gui/preferences/journals/AbbreviationsFileViewModel.java
+++ b/src/main/java/org/jabref/gui/preferences/journals/AbbreviationsFileViewModel.java
@@ -18,6 +18,7 @@
 import org.jabref.logic.journals.Abbreviation;
 import org.jabref.logic.journals.AbbreviationWriter;
 import org.jabref.logic.journals.JournalAbbreviationLoader;
+import org.jabref.logic.journals.JournalAbbreviationMvGenerator;
 
 /**
  * This class provides a model for abbreviation files. It actually doesn't save the files as objects but rather saves
@@ -57,6 +58,31 @@ public void readAbbreviations() throws IOException {
             throw new FileNotFoundException();
         }
     }
+    /**
+     * Reads journal abbreviations from the specified MV file and populates the abbreviations list.
+     * <p>
+     * If a valid file path is provided, this method loads abbreviations using
+     * {@link JournalAbbreviationMvGenerator#loadAbbreviationsFromMv(Path)} and converts them
+     * into {@link AbbreviationViewModel} objects before adding them to the  abbreviations list.
+     * </p>
+     *
+     * @throws IOException if the MV file cannot be found or read.
+     * @throws FileNotFoundException if no MV file path is specified.
+     */
+
+    public void readAbbreviationsFromMv() throws IOException {
+        if (path.isPresent()) {
+            // Load abbreviations from the MV file using MV processor.
+            Collection<Abbreviation> abbreviationList = JournalAbbreviationMvGenerator.loadAbbreviationsFromMv(path.get());
+
+            // Convert each Abbreviation into an AbbreviationViewModel and add it to the  abbreviations list.
+            for (Abbreviation abbreviation : abbreviationList) {
+                abbreviations.add(new AbbreviationViewModel(abbreviation));
+            }
+        } else {
+            throw new FileNotFoundException("MV file not specified");
+        }
+    }
 
     /**
      * This method will write all abbreviations of this abbreviation file to the file on the file system.
@@ -64,7 +90,7 @@ public void readAbbreviations() throws IOException {
      * {@link AbbreviationWriter#writeOrCreate}.
      */
     public void writeOrCreate() throws IOException {
-        if (!isBuiltInList.get()) {
+        if (!isBuiltInList.get() && !isMvFile()) {
             List<Abbreviation> actualAbbreviations =
                     abbreviations.stream().filter(abb -> !abb.isPseudoAbbreviation())
                                  .map(AbbreviationViewModel::getAbbreviationObject)
@@ -107,4 +133,8 @@ public boolean equals(Object obj) {
             return false;
         }
     }
+
+    public boolean isMvFile() {
+        return path.isPresent() && path.get().toString().endsWith(".mv");
+    }
 }
diff --git a/src/main/java/org/jabref/gui/preferences/journals/JournalAbbreviationsTab.fxml b/src/main/java/org/jabref/gui/preferences/journals/JournalAbbreviationsTab.fxml
index f909ee6a3d6..602241ce7eb 100644
--- a/src/main/java/org/jabref/gui/preferences/journals/JournalAbbreviationsTab.fxml
+++ b/src/main/java/org/jabref/gui/preferences/journals/JournalAbbreviationsTab.fxml
@@ -49,6 +49,11 @@
         </Button>
     </HBox>
 
+    <HBox spacing="10.0" alignment="CENTER_LEFT">
+        <Button fx:id="changeDirectoryButton" onAction="#handleChangeDirectory" text="Change Directory"/>
+        <CustomTextField fx:id="directoryPathField" editable="false" minWidth="250"/>
+    </HBox>
+
     <VBox spacing="10.0" HBox.hgrow="ALWAYS">
         <CustomTextField fx:id="searchBox" promptText="%Filter" VBox.vgrow="NEVER">
             <VBox.margin>
diff --git a/src/main/java/org/jabref/gui/preferences/journals/JournalAbbreviationsTab.java b/src/main/java/org/jabref/gui/preferences/journals/JournalAbbreviationsTab.java
index 5bfa504edf5..7bc6710cb97 100644
--- a/src/main/java/org/jabref/gui/preferences/journals/JournalAbbreviationsTab.java
+++ b/src/main/java/org/jabref/gui/preferences/journals/JournalAbbreviationsTab.java
@@ -60,6 +60,9 @@ public class JournalAbbreviationsTab extends AbstractPreferenceTabView<JournalAb
     @FXML private CustomTextField searchBox;
     @FXML private CheckBox useFJournal;
 
+    @FXML private Button changeDirectoryButton;
+    @FXML private CustomTextField directoryPathField;
+
     @Inject private TaskExecutor taskExecutor;
     @Inject private JournalAbbreviationRepository abbreviationRepository;
 
@@ -87,6 +90,7 @@ private void initialize() {
 
         searchBox.setPromptText(Localization.lang("Search..."));
         searchBox.setLeft(IconTheme.JabRefIcons.SEARCH.getGraphicNode());
+        directoryPathField.textProperty().bind(viewModel.directoryPathProperty());
     }
 
     private void setUpTable() {
@@ -190,6 +194,11 @@ private void editAbbreviation() {
                 journalTableNameColumn);
     }
 
+    @FXML
+    private void handleChangeDirectory() {
+        viewModel.handleChangeDirectory();
+    }
+
     private void selectNewAbbreviation() {
         int lastRow = viewModel.abbreviationsCountProperty().get() - 1;
         journalAbbreviationsTable.scrollTo(lastRow);
diff --git a/src/main/java/org/jabref/gui/preferences/journals/JournalAbbreviationsTabViewModel.java b/src/main/java/org/jabref/gui/preferences/journals/JournalAbbreviationsTabViewModel.java
index 7b95ac28244..ceb99266865 100644
--- a/src/main/java/org/jabref/gui/preferences/journals/JournalAbbreviationsTabViewModel.java
+++ b/src/main/java/org/jabref/gui/preferences/journals/JournalAbbreviationsTabViewModel.java
@@ -4,17 +4,21 @@
 import java.nio.file.Path;
 import java.util.List;
 import java.util.Objects;
+import java.util.Optional;
 import java.util.stream.Collectors;
 
 import javafx.beans.property.SimpleBooleanProperty;
 import javafx.beans.property.SimpleIntegerProperty;
 import javafx.beans.property.SimpleListProperty;
 import javafx.beans.property.SimpleObjectProperty;
+import javafx.beans.property.SimpleStringProperty;
+import javafx.beans.property.StringProperty;
 import javafx.collections.FXCollections;
 import javafx.collections.ListChangeListener;
 
 import org.jabref.gui.DialogService;
 import org.jabref.gui.preferences.PreferenceTabViewModel;
+import org.jabref.gui.util.DirectoryDialogConfiguration;
 import org.jabref.gui.util.FileDialogConfiguration;
 import org.jabref.logic.journals.Abbreviation;
 import org.jabref.logic.journals.JournalAbbreviationLoader;
@@ -57,6 +61,8 @@ public class JournalAbbreviationsTabViewModel implements PreferenceTabViewModel
     private final JournalAbbreviationRepository journalAbbreviationRepository;
     private boolean shouldWriteLists;
 
+    private final StringProperty directoryPath = new SimpleStringProperty();
+
     public JournalAbbreviationsTabViewModel(JournalAbbreviationPreferences abbreviationsPreferences,
                                             DialogService dialogService,
                                             TaskExecutor taskExecutor,
@@ -69,7 +75,7 @@ public JournalAbbreviationsTabViewModel(JournalAbbreviationPreferences abbreviat
         abbreviationsCount.bind(abbreviations.sizeProperty());
         currentAbbreviation.addListener((observable, oldValue, newValue) -> {
             boolean isAbbreviation = (newValue != null) && !newValue.isPseudoAbbreviation();
-            boolean isEditableFile = (currentFile.get() != null) && !currentFile.get().isBuiltInListProperty().get();
+            boolean isEditableFile = (currentFile.get() != null) && !currentFile.get().isBuiltInListProperty().get() && !currentFile.get().isMvFile();
             isEditableAndRemovable.set(isEditableFile);
             isAbbreviationEditableAndRemovable.set(isAbbreviation && isEditableFile);
         });
@@ -112,6 +118,8 @@ public void setValues() {
         createFileObjects();
         selectLastJournalFile();
         addBuiltInList();
+
+        directoryPath.set(abbreviationsPreferences.getJournalAbbreviationDir());
     }
 
     /**
@@ -119,7 +127,15 @@ public void setValues() {
      */
     public void createFileObjects() {
         List<String> externalFiles = abbreviationsPreferences.getExternalJournalLists();
-        externalFiles.forEach(name -> openFile(Path.of(name)));
+        externalFiles.forEach(name -> {
+            Path filePath = Path.of(name);
+
+            if (name.endsWith(".mv")) {
+                openMvFile(filePath);
+            } else { // .csv file
+                openFile(filePath);
+            }
+        });
     }
 
     /**
@@ -188,6 +204,23 @@ private void openFile(Path filePath) {
         journalFiles.add(abbreviationsFile);
     }
 
+    private void openMvFile(Path filePath) {
+        AbbreviationsFileViewModel abbreviationsFile = new AbbreviationsFileViewModel(filePath);
+        if (journalFiles.contains(abbreviationsFile)) {
+            dialogService.showErrorDialogAndWait(Localization.lang("Duplicated Journal File"),
+                    Localization.lang("Journal file %s already added", filePath.toString()));
+            return;
+        }
+        if (abbreviationsFile.exists()) {
+            try {
+                abbreviationsFile.readAbbreviationsFromMv();
+            } catch (IOException e) {
+                LOGGER.debug("Could not read abbreviations file", e);
+            }
+        }
+        journalFiles.add(abbreviationsFile);
+    }
+
     public void openFile() {
         FileDialogConfiguration fileDialogConfiguration = new FileDialogConfiguration.Builder()
                 .addExtensionFilter(StandardFileType.CSV)
@@ -215,6 +248,36 @@ public void removeCurrentFile() {
      * Method to add a new abbreviation to the abbreviations list property. It also sets the currentAbbreviation
      * property to the new abbreviation.
      */
+    public void handleChangeDirectory() {
+        DirectoryDialogConfiguration config = new DirectoryDialogConfiguration.Builder()
+                .withInitialDirectory(Path.of(directoryPath.get()))
+                .build();
+
+        Optional<Path> newDirectory = dialogService.showDirectorySelectionDialog(config);
+        newDirectory.ifPresent(path -> {
+            directoryPath.set(path.toString());
+            abbreviationsPreferences.setJournalAbbreviationDir(path.toString());
+            updateJournalFiles();
+            storeSettings();
+        });
+    }
+
+    private void updateJournalFiles() {
+        List<String> externalLists = abbreviationsPreferences.getExternalJournalLists();
+
+        // Remove files that are no longer in the external lists
+        journalFiles.removeIf(file -> !externalLists.contains(file.getAbsolutePath().map(Path::toString).orElse("")));
+        // Add new files
+        for (String filePath : externalLists) {
+            if (journalFiles.stream().noneMatch(file -> file.getAbsolutePath().map(Path::toString).orElse("").equals(filePath))) {
+                Path path = Path.of(filePath);
+                if (filePath.endsWith(".mv")) {
+                    openMvFile(path);
+                }
+            }
+        }
+    }
+
     public void addAbbreviation(Abbreviation abbreviationObject) {
         AbbreviationViewModel abbreviationViewModel = new AbbreviationViewModel(abbreviationObject);
         if (abbreviations.contains(abbreviationViewModel)) {
@@ -335,6 +398,7 @@ public void storeSettings() {
 
                     abbreviationsPreferences.setExternalJournalLists(journalStringList);
                     abbreviationsPreferences.setUseFJournalField(useFJournal.get());
+                    abbreviationsPreferences.setJournalAbbreviationDir(directoryPath.get());
 
                     if (shouldWriteLists) {
                         saveJournalAbbreviationFiles();
@@ -387,4 +451,8 @@ public SimpleBooleanProperty isFileRemovableProperty() {
     public SimpleBooleanProperty useFJournalProperty() {
         return useFJournal;
     }
+
+    public StringProperty directoryPathProperty() {
+        return directoryPath;
+    }
 }
diff --git a/src/main/java/org/jabref/logic/journals/JournalAbbreviationLoader.java b/src/main/java/org/jabref/logic/journals/JournalAbbreviationLoader.java
index 264a727ff13..37a981844a0 100644
--- a/src/main/java/org/jabref/logic/journals/JournalAbbreviationLoader.java
+++ b/src/main/java/org/jabref/logic/journals/JournalAbbreviationLoader.java
@@ -3,15 +3,20 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.nio.file.Files;
-import java.nio.file.InvalidPathException;
 import java.nio.file.Path;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
 
+import javafx.beans.property.SimpleStringProperty;
+
+import org.jabref.logic.util.Directories;
+
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import static org.jabref.logic.journals.JournalAbbreviationMvGenerator.loadAbbreviationsFromMv;
+
 /**
  * <p>
  *   This class loads abbreviations from a CSV file and stores them into a MV file ({@link #readAbbreviationsFromCsvFile(Path)}
@@ -34,7 +39,6 @@ public static Collection<Abbreviation> readAbbreviationsFromCsvFile(Path file) t
 
     public static JournalAbbreviationRepository loadRepository(JournalAbbreviationPreferences journalAbbreviationPreferences) {
         JournalAbbreviationRepository repository;
-
         // Initialize with built-in list
         try (InputStream resourceAsStream = JournalAbbreviationRepository.class.getResourceAsStream("/journals/journal-list.mv")) {
             if (resourceAsStream == null) {
@@ -53,6 +57,8 @@ public static JournalAbbreviationRepository loadRepository(JournalAbbreviationPr
             return null;
         }
 
+        journalAbbreviationPreferences.updateJournalsDir(journalAbbreviationPreferences.getJournalAbbreviationDir());
+
         // Read external lists
         List<String> lists = journalAbbreviationPreferences.getExternalJournalLists();
         // might produce NPE in tests
@@ -61,9 +67,13 @@ public static JournalAbbreviationRepository loadRepository(JournalAbbreviationPr
             Collections.reverse(lists);
             for (String filename : lists) {
                 try {
-                    repository.addCustomAbbreviations(readAbbreviationsFromCsvFile(Path.of(filename)));
-                } catch (IOException | InvalidPathException e) {
-                    // invalid path might come from unix/windows mixup of prefs
+                    Path filePath = Path.of(filename);
+                    if (filename.endsWith(".mv")) {
+                        repository.addCustomAbbreviations(loadAbbreviationsFromMv(filePath));
+                    } else if (filename.endsWith(".csv")) {
+                        repository.addCustomAbbreviations(readAbbreviationsFromCsvFile(filePath));
+                    }
+                } catch (IOException e) {
                     LOGGER.error("Cannot read external journal list file {}", filename, e);
                 }
             }
@@ -72,6 +82,6 @@ public static JournalAbbreviationRepository loadRepository(JournalAbbreviationPr
     }
 
     public static JournalAbbreviationRepository loadBuiltInRepository() {
-        return loadRepository(new JournalAbbreviationPreferences(Collections.emptyList(), true));
+        return loadRepository(new JournalAbbreviationPreferences(Collections.emptyList(), true, new SimpleStringProperty(Directories.getJournalAbbreviationsDirectory().toString())));
     }
 }
diff --git a/src/main/java/org/jabref/logic/journals/JournalAbbreviationMvGenerator.java b/src/main/java/org/jabref/logic/journals/JournalAbbreviationMvGenerator.java
new file mode 100644
index 00000000000..d13a8e38ff3
--- /dev/null
+++ b/src/main/java/org/jabref/logic/journals/JournalAbbreviationMvGenerator.java
@@ -0,0 +1,149 @@
+package org.jabref.logic.journals;
+
+import java.io.IOException;
+import java.nio.file.DirectoryStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import org.h2.mvstore.MVMap;
+import org.h2.mvstore.MVStore;
+import org.h2.mvstore.MVStoreException;
+import org.jooq.lambda.Unchecked;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This class converts each CSV abbreviation file in a given directory into an MV file.
+ * For each CSV file, an MV file is created in the same directory with the same base name.
+ */
+public class JournalAbbreviationMvGenerator {
+
+    private static final Logger LOGGER = LoggerFactory.getLogger(JournalAbbreviationMvGenerator.class);
+
+    /**
+     * Scans the given directory for CSV files and converts each
+     * CSV file into a corresponding MV file in the same directory.
+     *
+     * @param abbreviationsDirectory the directory containing journal abbreviation CSV files.
+     */
+    public static void convertAllCsvToMv(Path abbreviationsDirectory) {
+        // A set of filenames to ignore
+        Set<String> ignoredNames = Set.of(
+                "journal_abbreviations_entrez.csv",
+                "journal_abbreviations_medicus.csv",
+                "journal_abbreviations_webofscience-dotless.csv",
+                "journal_abbreviations_ieee_strings.csv"
+        );
+
+        // Open or create a persistent MVStore file for storing CSV file timestamps.
+        Path timestampFile = abbreviationsDirectory.resolve("timestamps.mv");
+        try (MVStore store = new MVStore.Builder()
+                .fileName(timestampFile.toString())
+                .compressHigh()
+                .open()) {
+            MVMap<String, Long> timestampMap = store.openMap("fileTimestamps");
+
+            // Iterate through all CSV files in the directory.
+            try (DirectoryStream<Path> stream = Files.newDirectoryStream(abbreviationsDirectory, "*.csv")) {
+                stream.forEach(Unchecked.consumer(csvFile -> {
+                    String fileName = csvFile.getFileName().toString();
+                    if (ignoredNames.contains(fileName)) {
+                        LOGGER.info("{} ignored", fileName);
+                    } else {
+                        long currentTimestamp = Files.getLastModifiedTime(csvFile).toMillis();
+                        Long storedTimestamp = timestampMap.get(fileName);
+
+                        // Compute the MV file path by replacing the .csv extension with .mv
+                        Path mvFile = csvFile.resolveSibling(fileName.replaceFirst("\\.csv$", ".mv"));
+
+                        // If MV file is missing OR the CSV file has been updated, process it
+                        if (!Files.exists(mvFile) || storedTimestamp == null || storedTimestamp != currentTimestamp) {
+                            convertCsvToMv(csvFile, mvFile);
+                            LOGGER.info("Processing {} -> Creating MV file: {}", fileName, mvFile.getFileName());
+                            // Update the timestamp in the persistent map
+                            timestampMap.put(fileName, currentTimestamp);
+                        } else {
+                            LOGGER.info("File {} is up-to-date, skipping conversion.", fileName);
+                        }
+                    }
+                }));
+            }
+            // Commit changes to the timestamp map
+            store.commit();
+        } catch (IOException e) {
+            LOGGER.error("Error while processing abbreviation files in directory: {}", abbreviationsDirectory, e);
+        }
+    }
+    /**
+     * Converts a CSV file into an MV file.
+     * Reads the CSV file and stores its abbreviations into an MVMap inside the MV file.
+     *
+     * @param csvFile the source CSV file.
+     * @param mvFile  the target MV file.
+     */
+
+    public static void convertCsvToMv(Path csvFile, Path mvFile) throws IOException {
+
+        try (MVStore store = new MVStore.Builder()
+                .fileName(mvFile.toString())
+                .compressHigh()
+                .open()) {
+            MVMap<String, Abbreviation> fullToAbbreviation = store.openMap("FullToAbbreviation");
+
+            // Clear the existing map to remove outdated entries
+            fullToAbbreviation.clear();
+
+            // Read abbreviations from the CSV file using existing logic
+            Collection<Abbreviation> abbreviations = JournalAbbreviationLoader.readAbbreviationsFromCsvFile(csvFile);
+
+            // Convert the collection into a map using the full journal name as the key
+            Map<String, Abbreviation> abbreviationMap = abbreviations
+                    .stream()
+                    .collect(Collectors.toMap(
+                            Abbreviation::getName,
+                            abbr -> abbr,
+                            (abbr1, abbr2) -> {
+                                LOGGER.info("Duplicate entry found: {}", abbr1.getName());
+                                return abbr2;
+                            }));
+
+            fullToAbbreviation.putAll(abbreviationMap);
+            store.commit();
+            LOGGER.info("Saved MV file: {}", mvFile.getFileName());
+        } catch (IOException e) {
+            LOGGER.error("Failed to convert CSV file: {}", csvFile, e);
+        }
+    }
+
+    public static Collection<Abbreviation> loadAbbreviationsFromMv(Path path) throws IOException {
+        Collection<Abbreviation> abbreviations = new ArrayList<>();
+
+            try (MVStore store = new MVStore.Builder()
+                .fileName(path.toString())
+                .readOnly()
+                .open()) {
+            MVMap<String, Abbreviation> abbreviationMap = store.openMap("FullToAbbreviation");
+
+            abbreviationMap.forEach((key, value) -> {
+                    Abbreviation fixedAbbreviation = new Abbreviation(
+                            key,
+                            value.getAbbreviation(),
+                            value.getShortestUniqueAbbreviation()
+                    );
+                    abbreviations.add(fixedAbbreviation);
+                });
+            store.commit();
+        } catch (MVStoreException e) {
+            LOGGER.error("MVStoreException: {} , Error message: {}", path, e.getMessage(), e);
+        } catch (Exception e) {
+            LOGGER.error("Unexpected error while reading MV file: {}", path, e);
+        }
+
+        return abbreviations;
+    }
+}
diff --git a/src/main/java/org/jabref/logic/journals/JournalAbbreviationPreferences.java b/src/main/java/org/jabref/logic/journals/JournalAbbreviationPreferences.java
index fb256bbe8ab..684abd29781 100644
--- a/src/main/java/org/jabref/logic/journals/JournalAbbreviationPreferences.java
+++ b/src/main/java/org/jabref/logic/journals/JournalAbbreviationPreferences.java
@@ -1,23 +1,96 @@
 package org.jabref.logic.journals;
 
+import java.io.IOException;
+import java.nio.file.DirectoryStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
 import java.util.List;
 
 import javafx.beans.property.BooleanProperty;
 import javafx.beans.property.SimpleBooleanProperty;
+import javafx.beans.property.SimpleStringProperty;
+import javafx.beans.property.StringProperty;
 import javafx.collections.FXCollections;
 import javafx.collections.ObservableList;
 
+import org.jabref.logic.util.Directories;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
 public class JournalAbbreviationPreferences {
+    private static final Logger LOGGER = LoggerFactory.getLogger(JournalAbbreviationPreferences.class);
 
+    private static final String CUSTOM_CSV_FILE = "custom.csv";
+    private static final String MV_FILE_PATTERN = "*.mv";
+    private static final String TIMESTAMPS_FILE = "timestamps.mv";
     private final ObservableList<String> externalJournalLists;
     private final BooleanProperty useFJournalField;
+    private StringProperty journalsDir;
 
     public JournalAbbreviationPreferences(List<String> externalJournalLists,
-                                          boolean useFJournalField) {
+                                          boolean useFJournalField,
+                                          StringProperty journalsDir) {
+
+        if (journalsDir == null || journalsDir.get() == null) {
+            this.journalsDir = new SimpleStringProperty(Directories.getJournalAbbreviationsDirectory().toString()); // default directory
+        } else {
+            this.journalsDir = journalsDir;
+        }
+
+        this.journalsDir.addListener((observable, oldValue, newValue) -> {
+            updateJournalsDir(newValue);
+        });
+
         this.externalJournalLists = FXCollections.observableArrayList(externalJournalLists);
         this.useFJournalField = new SimpleBooleanProperty(useFJournalField);
     }
 
+    public void updateJournalsDir(String directory) {
+        // Remove old .mv paths if exist
+        externalJournalLists.removeIf(path -> path.endsWith(".mv"));
+
+        Path dirPath = Path.of(directory);
+        try {
+            initializeDirectory(dirPath);
+        } catch (IOException e) {
+            LOGGER.error("Error initializing the journal directory", e);
+        }
+        setJournalAbbreviationDir(directory);
+    }
+
+    /**
+     * Ensures the journal abbreviation directory exists and initializes necessary files.
+     * <p>
+     * This method performs the following steps:
+     * - Creates the journal abbreviation directory if it does not already exist.
+     * - Ensures the existence of the "CUSTOM_CSV_FILE" file, creating an empty one if missing.
+     * - Converts all `.csv` files in the directory to `.mv` format using {@link JournalAbbreviationMvGenerator#convertAllCsvToMv}.
+     * - Scans the directory for `.mv` files (excluding TIMESTAMPS_FILE) and adds them to {@code externalJournalLists}.
+     * <p>
+     * If any I/O errors occur during these operations, they are logged.
+     *
+     * @param journalsDir The path to the journal abbreviation directory.
+     */
+    private void initializeDirectory(Path journalsDir) throws IOException {
+        Files.createDirectories(journalsDir);
+        Path customCsv = journalsDir.resolve(CUSTOM_CSV_FILE);
+        if (!Files.exists(customCsv)) {
+            Files.createFile(customCsv);
+        }
+
+        JournalAbbreviationMvGenerator.convertAllCsvToMv(journalsDir);
+
+        // Iterate through the directory and add all .mv files to externalJournalLists
+        try (DirectoryStream<Path> stream = Files.newDirectoryStream(journalsDir, MV_FILE_PATTERN)) {
+            for (Path mvFile : stream) {
+                if (!TIMESTAMPS_FILE.equals(mvFile.getFileName().toString())) { // Exclude TIMESTAMPS_FILE
+                    externalJournalLists.add(mvFile.toString());
+                }
+            }
+        }
+    }
+
     public ObservableList<String> getExternalJournalLists() {
         return externalJournalLists;
     }
@@ -38,4 +111,21 @@ public BooleanProperty useFJournalFieldProperty() {
     public void setUseFJournalField(boolean useFJournalField) {
         this.useFJournalField.set(useFJournalField);
     }
+
+    public String getJournalAbbreviationDir() {
+        if (journalsDir == null) {
+            String defaultDir = Directories.getJournalAbbreviationsDirectory().toString();
+            setJournalAbbreviationDir(defaultDir);
+            return defaultDir;
+        }
+        return journalsDir.get();
+    }
+
+    public StringProperty journalAbbreviationDirectoryProperty() {
+        return journalsDir;
+    }
+
+    public void setJournalAbbreviationDir(String journalsDir) {
+        this.journalsDir.set(journalsDir);
+    }
 }
diff --git a/src/main/java/org/jabref/logic/journals/JournalAbbreviationRepository.java b/src/main/java/org/jabref/logic/journals/JournalAbbreviationRepository.java
index 4191be45857..70c8ae2ce2c 100644
--- a/src/main/java/org/jabref/logic/journals/JournalAbbreviationRepository.java
+++ b/src/main/java/org/jabref/logic/journals/JournalAbbreviationRepository.java
@@ -39,15 +39,15 @@ public JournalAbbreviationRepository(Path journalList) {
         try (MVStore store = new MVStore.Builder().readOnly().fileName(journalList.toAbsolutePath().toString()).open()) {
             mvFullToAbbreviationObject = store.openMap("FullToAbbreviation");
             mvFullToAbbreviationObject.forEach((name, abbreviation) -> {
-                String abbrevationString = abbreviation.getAbbreviation();
+                String abbreviationString = abbreviation.getAbbreviation();
                 String shortestUniqueAbbreviation = abbreviation.getShortestUniqueAbbreviation();
                 Abbreviation newAbbreviation = new Abbreviation(
                         name,
-                        abbrevationString,
+                        abbreviationString,
                         shortestUniqueAbbreviation
                 );
                 fullToAbbreviationObject.put(name, newAbbreviation);
-                abbreviationToAbbreviationObject.put(abbrevationString, newAbbreviation);
+                abbreviationToAbbreviationObject.put(abbreviationString, newAbbreviation);
                 dotlessToAbbreviationObject.put(newAbbreviation.getDotlessAbbreviation(), newAbbreviation);
                 shortestUniqueToAbbreviationObject.put(shortestUniqueAbbreviation, newAbbreviation);
             });
diff --git a/src/main/java/org/jabref/logic/preferences/JabRefCliPreferences.java b/src/main/java/org/jabref/logic/preferences/JabRefCliPreferences.java
index dd9832448bb..6bc92c8ec94 100644
--- a/src/main/java/org/jabref/logic/preferences/JabRefCliPreferences.java
+++ b/src/main/java/org/jabref/logic/preferences/JabRefCliPreferences.java
@@ -28,6 +28,7 @@
 import java.util.stream.Stream;
 
 import javafx.beans.InvalidationListener;
+import javafx.beans.property.SimpleStringProperty;
 import javafx.collections.ListChangeListener;
 import javafx.collections.SetChangeListener;
 
@@ -328,6 +329,8 @@ public class JabRefCliPreferences implements CliPreferences {
     private static final String EXTERNAL_JOURNAL_LISTS = "externalJournalLists";
     private static final String USE_AMS_FJOURNAL = "useAMSFJournal";
 
+    private static final String JOURNAL_ABBREVIATION_DIRECTORY = "journalAbbreviationDirectory";
+
     // Protected terms
     private static final String PROTECTED_TERMS_ENABLED_EXTERNAL = "protectedTermsEnabledExternal";
     private static final String PROTECTED_TERMS_DISABLED_EXTERNAL = "protectedTermsDisabledExternal";
@@ -577,7 +580,7 @@ protected JabRefCliPreferences() {
         defaults.put(CONFIRM_LINKED_FILE_DELETE, Boolean.TRUE);
         defaults.put(KEEP_DOWNLOAD_URL, Boolean.TRUE);
         defaults.put(DEFAULT_CITATION_KEY_PATTERN, "[auth][year]");
-        defaults.put(UNWANTED_CITATION_KEY_CHARACTERS, "-`ʹ:!;?^$");
+        defaults.put(UNWANTED_CITATION_KEY_CHARACTERS, "-`ʹ:!;?^");
         defaults.put(RESOLVE_STRINGS_FOR_FIELDS, "author;booktitle;editor;editora;editorb;editorc;institution;issuetitle;journal;journalsubtitle;journaltitle;mainsubtitle;month;publisher;shortauthor;shorteditor;subtitle;titleaddon");
         defaults.put(DO_NOT_RESOLVE_STRINGS, Boolean.FALSE);
         defaults.put(NON_WRAPPABLE_FIELDS, "pdf;ps;url;doi;file;isbn;issn");
@@ -673,6 +676,9 @@ protected JabRefCliPreferences() {
         // endregion
 
         // endregion
+
+        // Journal abbreviations directory
+        defaults.put(JOURNAL_ABBREVIATION_DIRECTORY, Directories.getJournalAbbreviationsDirectory().toString());
     }
 
     public void setLanguageDependentDefaultValues() {
@@ -1012,13 +1018,19 @@ public JournalAbbreviationPreferences getJournalAbbreviationPreferences() {
 
         journalAbbreviationPreferences = new JournalAbbreviationPreferences(
                 getStringList(EXTERNAL_JOURNAL_LISTS),
-                getBoolean(USE_AMS_FJOURNAL));
+                getBoolean(USE_AMS_FJOURNAL),
+               new SimpleStringProperty(get(JOURNAL_ABBREVIATION_DIRECTORY)));
 
         journalAbbreviationPreferences.getExternalJournalLists().addListener((InvalidationListener) change ->
                 putStringList(EXTERNAL_JOURNAL_LISTS, journalAbbreviationPreferences.getExternalJournalLists()));
         EasyBind.listen(journalAbbreviationPreferences.useFJournalFieldProperty(),
                 (obs, oldValue, newValue) -> putBoolean(USE_AMS_FJOURNAL, newValue));
 
+        EasyBind.listen(journalAbbreviationPreferences.journalAbbreviationDirectoryProperty(),
+                (obs, oldValue, newValue) -> {
+                    put(JOURNAL_ABBREVIATION_DIRECTORY, newValue);
+                });
+
         return journalAbbreviationPreferences;
     }
 
diff --git a/src/main/java/org/jabref/logic/util/Directories.java b/src/main/java/org/jabref/logic/util/Directories.java
index e87666d749a..7d7e2c4cb67 100644
--- a/src/main/java/org/jabref/logic/util/Directories.java
+++ b/src/main/java/org/jabref/logic/util/Directories.java
@@ -62,4 +62,11 @@ public static Path getSslDirectory() {
                                              "ssl",
                                              OS.APP_DIR_APP_AUTHOR));
     }
+
+    public static Path getJournalAbbreviationsDirectory() {
+        return Path.of(AppDirsFactory.getInstance()
+                                     .getUserDataDir(OS.APP_DIR_APP_NAME,
+                                             "journal-abbreviations",
+                                             OS.APP_DIR_APP_AUTHOR));
+    }
 }
diff --git a/src/test/java/org/jabref/logic/journals/JournalAbbreviationMvGeneratorTest.java b/src/test/java/org/jabref/logic/journals/JournalAbbreviationMvGeneratorTest.java
new file mode 100644
index 00000000000..916bc4b7fc3
--- /dev/null
+++ b/src/test/java/org/jabref/logic/journals/JournalAbbreviationMvGeneratorTest.java
@@ -0,0 +1,81 @@
+package org.jabref.logic.journals;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Collection;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class JournalAbbreviationMvGeneratorTest {
+
+    @TempDir
+    Path tempDir;
+
+    private Path csvFile;
+    private Path mvFile;
+
+    @BeforeEach
+    void setUp() throws IOException {
+        // Create a sample CSV file with two entries.
+        csvFile = tempDir.resolve("testJournal.csv");
+        String csvContent = "\"Test Journal\",\"T. J.\"\n" +
+                "\"Another Journal\",\"A. J.\"";
+        Files.writeString(csvFile, csvContent);
+
+        // The expected MV file has the same base name with extension .mv.
+        mvFile = tempDir.resolve("testJournal.mv");
+    }
+
+    @Test
+    void convertCsvToMvCreatesMvFileAndLoadsCorrectly() throws IOException, InterruptedException {
+        // Convert CSV to MV
+        JournalAbbreviationMvGenerator.convertCsvToMv(csvFile, mvFile);
+
+        // Verify the MV file is created
+        assertTrue(Files.exists(mvFile));
+
+        // Load abbreviations from the MV file
+        Collection<Abbreviation> abbreviations = JournalAbbreviationMvGenerator.loadAbbreviationsFromMv(mvFile);
+        // Expecting 2 abbreviations as in the CSV file
+        assertEquals(2, abbreviations.size());
+
+        // Check that the abbreviations match expected values
+        boolean foundTestJournal = abbreviations.stream()
+                                                .anyMatch(abbr -> "Test Journal".equalsIgnoreCase(abbr.getName()) &&
+                                                        "T. J.".equals(abbr.getAbbreviation()));
+        boolean foundAnotherJournal = abbreviations.stream()
+                                                   .anyMatch(abbr -> "Another Journal".equalsIgnoreCase(abbr.getName()) &&
+                                                           "A. J.".equals(abbr.getAbbreviation()));
+        assertTrue(foundTestJournal);
+        assertTrue(foundAnotherJournal);
+    }
+
+    @Test
+    void convertAllCsvToMvIgnoresSpecifiedFiles(@TempDir Path testDir) throws IOException {
+        // Create an ignored CSV file.
+        Path ignoredCsv = testDir.resolve("journal_abbreviations_entrez.csv");
+        Files.writeString(ignoredCsv, "\"Ignored Journal\",\"I. J.\"");
+
+        // Create a valid CSV file.
+        Path validCsv = testDir.resolve("validJournal.csv");
+        Files.writeString(validCsv, "\"Valid Journal\",\"V. J.\"");
+
+        // Run convertAllCsvToMv on the test directory.
+        JournalAbbreviationMvGenerator.convertAllCsvToMv(testDir);
+
+        // The ignored CSV file should not produce an MV file.
+        Path ignoredMv = testDir.resolve("journal_abbreviations_entrez.mv");
+        assertFalse(Files.exists(ignoredMv));
+
+        // The valid CSV file should produce an MV file.
+        Path validMv = testDir.resolve("validJournal.mv");
+        assertTrue(Files.exists(validMv));
+    }
+}

From 5a2194a992147cff460cd17126a2007ca01408f9 Mon Sep 17 00:00:00 2001
From: Adham Ahmed <42949982+adhamahmad@users.noreply.github.com>
Date: Fri, 28 Mar 2025 04:52:21 +0200
Subject: [PATCH 2/2] Update
 src/main/java/org/jabref/gui/preferences/journals/JournalAbbreviationsTabViewModel.java

Co-authored-by: Ruslan <ruslanpopov1512@gmail.com>
---
 .../preferences/journals/JournalAbbreviationsTabViewModel.java  | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/main/java/org/jabref/gui/preferences/journals/JournalAbbreviationsTabViewModel.java b/src/main/java/org/jabref/gui/preferences/journals/JournalAbbreviationsTabViewModel.java
index ceb99266865..764f0fec1a5 100644
--- a/src/main/java/org/jabref/gui/preferences/journals/JournalAbbreviationsTabViewModel.java
+++ b/src/main/java/org/jabref/gui/preferences/journals/JournalAbbreviationsTabViewModel.java
@@ -208,7 +208,7 @@ private void openMvFile(Path filePath) {
         AbbreviationsFileViewModel abbreviationsFile = new AbbreviationsFileViewModel(filePath);
         if (journalFiles.contains(abbreviationsFile)) {
             dialogService.showErrorDialogAndWait(Localization.lang("Duplicated Journal File"),
-                    Localization.lang("Journal file %s already added", filePath.toString()));
+                    Localization.lang("Journal file %s is already added", filePath.toString()));
             return;
         }
         if (abbreviationsFile.exists()) {