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()) {