diff --git a/CHANGELOG.md b/CHANGELOG.md index ee7d2710896..740dc74e40e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv - We added a "LTWA" abbreviation feature in the "Quality > Abbreviate journal names > LTWA" menu [#12273](https://github.com/JabRef/jabref/issues/12273/) - We added path validation to file directories in library properties dialog. [#11840](https://github.com/JabRef/jabref/issues/11840) - We now support usage of custom CSL styles in the Open/LibreOffice integration. [#12337](https://github.com/JabRef/jabref/issues/12337) +- We added ability to toggle journal abbreviation lists (including built-in and external CSV files) on/off in preferences. [#12468](https://github.com/JabRef/jabref/pull/12468) - We added support for citation-only CSL styles which don't specify bibliography formatting. [#12996](https://github.com/JabRef/jabref/pull/12996) ### Changed diff --git a/src/main/java/org/jabref/gui/frame/MainMenu.java b/src/main/java/org/jabref/gui/frame/MainMenu.java index 5e640fb3294..bfb22874032 100644 --- a/src/main/java/org/jabref/gui/frame/MainMenu.java +++ b/src/main/java/org/jabref/gui/frame/MainMenu.java @@ -267,12 +267,12 @@ private void createMenu() { new SeparatorMenuItem(), factory.createSubMenu(StandardActions.ABBREVIATE, - factory.createMenuItem(StandardActions.ABBREVIATE_DEFAULT, new AbbreviateAction(StandardActions.ABBREVIATE_DEFAULT, frame::getCurrentLibraryTab, dialogService, stateManager, preferences.getJournalAbbreviationPreferences(), abbreviationRepository, taskExecutor, undoManager)), - factory.createMenuItem(StandardActions.ABBREVIATE_DOTLESS, new AbbreviateAction(StandardActions.ABBREVIATE_DOTLESS, frame::getCurrentLibraryTab, dialogService, stateManager, preferences.getJournalAbbreviationPreferences(), abbreviationRepository, taskExecutor, undoManager)), - factory.createMenuItem(StandardActions.ABBREVIATE_SHORTEST_UNIQUE, new AbbreviateAction(StandardActions.ABBREVIATE_SHORTEST_UNIQUE, frame::getCurrentLibraryTab, dialogService, stateManager, preferences.getJournalAbbreviationPreferences(), abbreviationRepository, taskExecutor, undoManager)), - factory.createMenuItem(StandardActions.ABBREVIATE_LTWA, new AbbreviateAction(StandardActions.ABBREVIATE_LTWA, frame::getCurrentLibraryTab, dialogService, stateManager, preferences.getJournalAbbreviationPreferences(), abbreviationRepository, taskExecutor, undoManager))), + factory.createMenuItem(StandardActions.ABBREVIATE_DEFAULT, new AbbreviateAction(StandardActions.ABBREVIATE_DEFAULT, frame::getCurrentLibraryTab, dialogService, stateManager, preferences.getJournalAbbreviationPreferences(), taskExecutor, undoManager)), + factory.createMenuItem(StandardActions.ABBREVIATE_DOTLESS, new AbbreviateAction(StandardActions.ABBREVIATE_DOTLESS, frame::getCurrentLibraryTab, dialogService, stateManager, preferences.getJournalAbbreviationPreferences(), taskExecutor, undoManager)), + factory.createMenuItem(StandardActions.ABBREVIATE_SHORTEST_UNIQUE, new AbbreviateAction(StandardActions.ABBREVIATE_SHORTEST_UNIQUE, frame::getCurrentLibraryTab, dialogService, stateManager, preferences.getJournalAbbreviationPreferences(), taskExecutor, undoManager)), + factory.createMenuItem(StandardActions.ABBREVIATE_LTWA, new AbbreviateAction(StandardActions.ABBREVIATE_LTWA, frame::getCurrentLibraryTab, dialogService, stateManager, preferences.getJournalAbbreviationPreferences(), taskExecutor, undoManager))), - factory.createMenuItem(StandardActions.UNABBREVIATE, new AbbreviateAction(StandardActions.UNABBREVIATE, frame::getCurrentLibraryTab, dialogService, stateManager, preferences.getJournalAbbreviationPreferences(), abbreviationRepository, taskExecutor, undoManager)) + factory.createMenuItem(StandardActions.UNABBREVIATE, new AbbreviateAction(StandardActions.UNABBREVIATE, frame::getCurrentLibraryTab, dialogService, stateManager, preferences.getJournalAbbreviationPreferences(), taskExecutor, undoManager)) ); Menu lookupIdentifiers = factory.createSubMenu(StandardActions.LOOKUP_DOC_IDENTIFIER); diff --git a/src/main/java/org/jabref/gui/journals/AbbreviateAction.java b/src/main/java/org/jabref/gui/journals/AbbreviateAction.java index edb54c40529..562decce5d2 100644 --- a/src/main/java/org/jabref/gui/journals/AbbreviateAction.java +++ b/src/main/java/org/jabref/gui/journals/AbbreviateAction.java @@ -1,6 +1,9 @@ package org.jabref.gui.journals; +import java.util.ArrayList; import java.util.List; +import java.util.Map; +import java.util.Optional; import java.util.function.Supplier; import javax.swing.undo.UndoManager; @@ -12,6 +15,8 @@ import org.jabref.gui.actions.SimpleCommand; import org.jabref.gui.actions.StandardActions; import org.jabref.gui.undo.NamedCompound; +import org.jabref.logic.journals.Abbreviation; +import org.jabref.logic.journals.JournalAbbreviationLoader; import org.jabref.logic.journals.JournalAbbreviationPreferences; import org.jabref.logic.journals.JournalAbbreviationRepository; import org.jabref.logic.l10n.Localization; @@ -19,6 +24,7 @@ import org.jabref.logic.util.TaskExecutor; import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.field.Field; import org.jabref.model.entry.field.FieldFactory; import org.slf4j.Logger; @@ -36,9 +42,12 @@ public class AbbreviateAction extends SimpleCommand { private final DialogService dialogService; private final StateManager stateManager; private final JournalAbbreviationPreferences journalAbbreviationPreferences; - private final JournalAbbreviationRepository abbreviationRepository; + private JournalAbbreviationRepository abbreviationRepository; private final TaskExecutor taskExecutor; private final UndoManager undoManager; + + private JournalAbbreviationPreferences lastUsedPreferences; + private JournalAbbreviationRepository cachedRepository; private AbbreviationType abbreviationType; @@ -47,7 +56,6 @@ public AbbreviateAction(StandardActions action, DialogService dialogService, StateManager stateManager, JournalAbbreviationPreferences abbreviationPreferences, - JournalAbbreviationRepository abbreviationRepository, TaskExecutor taskExecutor, UndoManager undoManager) { this.action = action; @@ -55,7 +63,6 @@ public AbbreviateAction(StandardActions action, this.dialogService = dialogService; this.stateManager = stateManager; this.journalAbbreviationPreferences = abbreviationPreferences; - this.abbreviationRepository = abbreviationRepository; this.taskExecutor = taskExecutor; this.undoManager = undoManager; @@ -75,8 +82,18 @@ public AbbreviateAction(StandardActions action, this.executable.bind(ActionHelper.needsEntriesSelected(stateManager)); } + /** + * Executes the abbreviation or unabbreviation action on selected entries. + * For unabbreviation, checks if any journal source is enabled first. + * For abbreviation, handles different abbreviation types (default, dotless, shortest unique, LTWA). + */ @Override public void execute() { + if (action == StandardActions.UNABBREVIATE && !journalAbbreviationPreferences.areAnyJournalSourcesEnabled()) { + dialogService.notify(Localization.lang("Cannot unabbreviate: all journal lists are disabled.")); + return; + } + if ((action == StandardActions.ABBREVIATE_DEFAULT) || (action == StandardActions.ABBREVIATE_DOTLESS) || (action == StandardActions.ABBREVIATE_SHORTEST_UNIQUE) @@ -98,6 +115,8 @@ public void execute() { } private String abbreviate(BibDatabaseContext databaseContext, List<BibEntry> entries) { + abbreviationRepository = getRepository(); + UndoableAbbreviator undoableAbbreviator = new UndoableAbbreviator( abbreviationRepository, abbreviationType, @@ -119,13 +138,46 @@ private String abbreviate(BibDatabaseContext databaseContext, List<BibEntry> ent return Localization.lang("Abbreviated %0 journal names.", String.valueOf(count)); } + /** + * Unabbreviate journal names in entries, respecting the enabled/disabled state of sources. + * Only unabbreviates entries from enabled sources. + */ private String unabbreviate(BibDatabaseContext databaseContext, List<BibEntry> entries) { - UndoableUnabbreviator undoableAbbreviator = new UndoableUnabbreviator(abbreviationRepository); - + List<BibEntry> filteredEntries = new ArrayList<>(); + + JournalAbbreviationRepository freshRepository = getRepository(); + + for (BibEntry entry : entries) { + boolean includeEntry = true; + + for (Field journalField : FieldFactory.getJournalNameFields()) { + if (!entry.hasField(journalField)) { + continue; + } + + String text = entry.getFieldLatexFree(journalField).orElse(""); + + if (freshRepository.isAbbreviatedName(text)) { + Optional<Abbreviation> abbreviation = freshRepository.getForUnabbreviation(text); + + if (abbreviation.isEmpty()) { + includeEntry = false; + break; + } + } + } + + if (includeEntry) { + filteredEntries.add(entry); + } + } + + UndoableUnabbreviator undoableAbbreviator = new UndoableUnabbreviator(freshRepository); NamedCompound ce = new NamedCompound(Localization.lang("Unabbreviate journal names")); - int count = entries.stream().mapToInt(entry -> + int count = filteredEntries.stream().mapToInt(entry -> (int) FieldFactory.getJournalNameFields().stream().filter(journalField -> undoableAbbreviator.unabbreviate(databaseContext.getDatabase(), entry, journalField, ce)).count()).sum(); + if (count == 0) { return Localization.lang("No journal names could be unabbreviated."); } @@ -135,4 +187,80 @@ private String unabbreviate(BibDatabaseContext databaseContext, List<BibEntry> e tabSupplier.get().markBaseChanged(); return Localization.lang("Unabbreviated %0 journal names.", String.valueOf(count)); } + + /** + * Gets a repository instance, using cached version if preferences haven't changed. + * This provides efficient repository creation without using static/singleton patterns. + * + * + * @return A repository configured with current preferences + */ + private JournalAbbreviationRepository getRepository() { + if (cachedRepository != null && !preferencesChanged()) { + return cachedRepository; + } + + cachedRepository = JournalAbbreviationLoader.loadRepository(journalAbbreviationPreferences); + lastUsedPreferences = clonePreferences(); + return cachedRepository; + } + + /** + * Checks if preferences have changed since last repository creation + * + * + * @return true if preferences have changed, false otherwise + */ + private boolean preferencesChanged() { + if (lastUsedPreferences == null) { + return true; + } + + if (lastUsedPreferences.shouldUseFJournalField() != journalAbbreviationPreferences.shouldUseFJournalField()) { + return true; + } + + List<String> oldLists = lastUsedPreferences.getExternalJournalLists(); + List<String> newLists = journalAbbreviationPreferences.getExternalJournalLists(); + + if (oldLists.size() != newLists.size()) { + return true; + } + + for (int i = 0; i < oldLists.size(); i++) { + if (!oldLists.get(i).equals(newLists.get(i))) { + return true; + } + } + + Map<String, Boolean> oldEnabled = lastUsedPreferences.getEnabledExternalLists(); + Map<String, Boolean> newEnabled = journalAbbreviationPreferences.getEnabledExternalLists(); + + if (oldEnabled.size() != newEnabled.size()) { + return true; + } + + for (Map.Entry<String, Boolean> entry : newEnabled.entrySet()) { + Boolean oldValue = oldEnabled.get(entry.getKey()); + if (!java.util.Objects.equals(oldValue, entry.getValue())) { + return true; + } + } + + return false; + } + + /** + * Creates a clone of the current preferences for comparison + * + * + * @return A new preferences instance with the same settings + */ + private JournalAbbreviationPreferences clonePreferences() { + return new JournalAbbreviationPreferences( + journalAbbreviationPreferences.getExternalJournalLists(), + journalAbbreviationPreferences.shouldUseFJournalField(), + journalAbbreviationPreferences.getEnabledExternalLists() + ); + } } diff --git a/src/main/java/org/jabref/gui/journals/UndoableAbbreviator.java b/src/main/java/org/jabref/gui/journals/UndoableAbbreviator.java index b4cb7199a93..7269022a417 100644 --- a/src/main/java/org/jabref/gui/journals/UndoableAbbreviator.java +++ b/src/main/java/org/jabref/gui/journals/UndoableAbbreviator.java @@ -27,6 +27,8 @@ public UndoableAbbreviator(JournalAbbreviationRepository journalAbbreviationRepo /** * Abbreviate the journal name of the given entry. + * This method respects the enabled/disabled state of journal abbreviation sources. + * If a journal name comes from a disabled source, it will not be abbreviated. * * @param database The database the entry belongs to, or null if no database. * @param entry The entry to be treated. @@ -42,7 +44,7 @@ public boolean abbreviate(BibDatabase database, BibEntry entry, Field fieldName, String origText = entry.getField(fieldName).get(); String text = database != null ? database.resolveForStrings(origText) : origText; - Optional<Abbreviation> foundAbbreviation = journalAbbreviationRepository.get(text); + Optional<Abbreviation> foundAbbreviation = journalAbbreviationRepository.getForAbbreviation(text); if (foundAbbreviation.isEmpty() && abbreviationType != AbbreviationType.LTWA) { return false; // Unknown, cannot abbreviate anything. diff --git a/src/main/java/org/jabref/gui/journals/UndoableUnabbreviator.java b/src/main/java/org/jabref/gui/journals/UndoableUnabbreviator.java index acb101aef58..6f0103b10d9 100644 --- a/src/main/java/org/jabref/gui/journals/UndoableUnabbreviator.java +++ b/src/main/java/org/jabref/gui/journals/UndoableUnabbreviator.java @@ -21,6 +21,8 @@ public UndoableUnabbreviator(JournalAbbreviationRepository journalAbbreviationRe /** * Unabbreviate the journal name of the given entry. + * This method respects the enabled/disabled state of journal abbreviation sources. + * If an abbreviation comes from a disabled source, it will not be unabbreviated. * * @param entry The entry to be treated. * @param field The field @@ -41,16 +43,17 @@ public boolean unabbreviate(BibDatabase database, BibEntry entry, Field field, C if (database != null) { text = database.resolveForStrings(text); } - - if (!journalAbbreviationRepository.isKnownName(text)) { - return false; // Cannot do anything if it is not known. - } - + if (!journalAbbreviationRepository.isAbbreviatedName(text)) { return false; // Cannot unabbreviate unabbreviated name. } + + var abbreviationOpt = journalAbbreviationRepository.getForUnabbreviation(text); + if (abbreviationOpt.isEmpty()) { + return false; + } - Abbreviation abbreviation = journalAbbreviationRepository.get(text).get(); + Abbreviation abbreviation = abbreviationOpt.get(); String newText = abbreviation.getName(); entry.setField(field, newText); ce.addEdit(new UndoableFieldChange(entry, field, origText, newText)); 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..2e15aeffe34 100644 --- a/src/main/java/org/jabref/gui/preferences/journals/AbbreviationsFileViewModel.java +++ b/src/main/java/org/jabref/gui/preferences/journals/AbbreviationsFileViewModel.java @@ -1,110 +1,187 @@ package org.jabref.gui.preferences.journals; -import java.io.FileNotFoundException; import java.io.IOException; import java.nio.file.Files; +import java.nio.file.NoSuchFileException; import java.nio.file.Path; -import java.util.Collection; import java.util.List; import java.util.Objects; import java.util.Optional; -import java.util.stream.Collectors; +import java.util.Set; +import javafx.beans.property.BooleanProperty; import javafx.beans.property.ReadOnlyBooleanProperty; +import javafx.beans.property.ReadOnlyBooleanWrapper; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleListProperty; +import javafx.beans.property.SimpleStringProperty; import javafx.collections.FXCollections; import org.jabref.logic.journals.Abbreviation; import org.jabref.logic.journals.AbbreviationWriter; import org.jabref.logic.journals.JournalAbbreviationLoader; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + /** * This class provides a model for abbreviation files. It actually doesn't save the files as objects but rather saves * their paths. This also allows to specify pseudo files as placeholder objects. */ public class AbbreviationsFileViewModel { + private static final Logger LOGGER = LoggerFactory.getLogger(AbbreviationsFileViewModel.class); - private final SimpleListProperty<AbbreviationViewModel> abbreviations = new SimpleListProperty<>( - FXCollections.observableArrayList()); - private final ReadOnlyBooleanProperty isBuiltInList; - private final String name; - private final Optional<Path> path; - - public AbbreviationsFileViewModel(Path filePath) { - this.path = Optional.ofNullable(filePath); - this.name = path.get().toAbsolutePath().toString(); - this.isBuiltInList = new SimpleBooleanProperty(false); - } + private final SimpleStringProperty name = new SimpleStringProperty(); + private final ReadOnlyBooleanWrapper isBuiltInList = new ReadOnlyBooleanWrapper(); + private final SimpleListProperty<AbbreviationViewModel> abbreviations = new SimpleListProperty<>(FXCollections.observableArrayList()); + private final Path filePath; + private final SimpleBooleanProperty enabled = new SimpleBooleanProperty(true); /** - * This constructor should only be called to create a pseudo abbreviation file for built in lists. This means it is - * a placeholder and its path will be null meaning it has no place on the filesystem. Its isPseudoFile property - * will therefore be set to true. + * This creates a built in list containing the abbreviations from the given list + * + * @param name The name of the built in list */ public AbbreviationsFileViewModel(List<AbbreviationViewModel> abbreviations, String name) { this.abbreviations.addAll(abbreviations); - this.name = name; - this.path = Optional.empty(); - this.isBuiltInList = new SimpleBooleanProperty(true); + this.name.setValue(name); + this.isBuiltInList.setValue(true); + this.filePath = null; + } + + public AbbreviationsFileViewModel(Path filePath) { + this.name.setValue(filePath.getFileName().toString()); + this.filePath = filePath; + this.isBuiltInList.setValue(false); + } + + public boolean exists() { + return isBuiltInList.get() || Files.exists(filePath); + } + + public SimpleStringProperty nameProperty() { + return name; } + public ReadOnlyBooleanProperty isBuiltInListProperty() { + return isBuiltInList.getReadOnlyProperty(); + } + + public SimpleListProperty<AbbreviationViewModel> abbreviationsProperty() { + return abbreviations; + } + + /** + * Reads journal abbreviations from the associated file and updates the abbreviations list. + * For built-in lists, this method does nothing and returns immediately. + * For file-based lists, it attempts to read abbreviations from the CSV file at the specified path. + * If the file doesn't exist, a debug message is logged but no exception is propagated. + * + * @throws IOException If there is an error reading the abbreviation file + */ public void readAbbreviations() throws IOException { - if (path.isPresent()) { - Collection<Abbreviation> abbreviationList = JournalAbbreviationLoader.readAbbreviationsFromCsvFile(path.get()); - abbreviationList.forEach(abbreviation -> abbreviations.addAll(new AbbreviationViewModel(abbreviation))); - } else { - throw new FileNotFoundException(); + if (isBuiltInList.get()) { + return; + } + try { + Set<Abbreviation> abbreviationsFromFile = JournalAbbreviationLoader.readAbbreviationsFromCsvFile(filePath); + + List<AbbreviationViewModel> viewModels = abbreviationsFromFile.stream() + .map(AbbreviationViewModel::new) + .toList(); + abbreviations.setAll(viewModels); + } catch (NoSuchFileException e) { + LOGGER.debug("Journal abbreviation list {} does not exist", filePath, e); } } /** - * This method will write all abbreviations of this abbreviation file to the file on the file system. - * It essentially will check if the current file is a builtin list and if not it will call - * {@link AbbreviationWriter#writeOrCreate}. + * Writes the abbreviations to the associated file or creates a new file if it doesn't exist. + * For built-in lists, this method does nothing and returns immediately. + * For file-based lists, it collects all abbreviations from the property and writes them to the file. + * + * @throws IOException If there is an error writing to the file */ public void writeOrCreate() throws IOException { - if (!isBuiltInList.get()) { - List<Abbreviation> actualAbbreviations = - abbreviations.stream().filter(abb -> !abb.isPseudoAbbreviation()) - .map(AbbreviationViewModel::getAbbreviationObject) - .collect(Collectors.toList()); - AbbreviationWriter.writeOrCreate(path.get(), actualAbbreviations); + if (isBuiltInList.get()) { + return; } + + List<Abbreviation> abbreviationList = abbreviationsProperty().stream() + .map(AbbreviationViewModel::getAbbreviationObject) + .toList(); + AbbreviationWriter.writeOrCreate(filePath, abbreviationList); } - public SimpleListProperty<AbbreviationViewModel> abbreviationsProperty() { - return abbreviations; + /** + * Gets the absolute path of this abbreviation file + * + * @return The optional absolute path of the file, or empty if it's a built-in list + */ + public Optional<Path> getAbsolutePath() { + if (isBuiltInList.get()) { + return Optional.empty(); + } + + try { + Path normalizedPath = filePath.toAbsolutePath().normalize(); + return Optional.of(normalizedPath); + } catch (Exception e) { + return Optional.of(filePath); + } } - - public boolean exists() { - return path.isPresent() && Files.exists(path.get()); + + /** + * Checks if this source is enabled + * + * @return true if the source is enabled + */ + public boolean isEnabled() { + return enabled.get(); } - - public Optional<Path> getAbsolutePath() { - return path; + + /** + * Sets the enabled state of this source + * + * @param enabled true to enable the source, false to disable it + */ + public void setEnabled(boolean enabled) { + this.enabled.set(enabled); } - - public ReadOnlyBooleanProperty isBuiltInListProperty() { - return isBuiltInList; + + /** + * Gets the enabled property for binding + * + * @return the enabled property + */ + public BooleanProperty enabledProperty() { + return enabled; } @Override - public String toString() { - return name; + public boolean equals(Object o) { + if (this == o) { + return true; + } + if ((o == null) || (getClass() != o.getClass())) { + return false; + } + AbbreviationsFileViewModel viewModel = (AbbreviationsFileViewModel) o; + if (isBuiltInList.get() && viewModel.isBuiltInList.get()) { + return name.get().equals(viewModel.name.get()); + } + return !isBuiltInList.get() && !viewModel.isBuiltInList.get() && + Objects.equals(filePath, viewModel.filePath); } @Override public int hashCode() { - return Objects.hash(name); + return Objects.hash(isBuiltInList, filePath); } @Override - public boolean equals(Object obj) { - if (obj instanceof AbbreviationsFileViewModel model) { - return Objects.equals(this.name, model.name); - } else { - return false; - } + public String toString() { + return name.get(); } } + 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..810b4c77fd4 100644 --- a/src/main/java/org/jabref/gui/preferences/journals/JournalAbbreviationsTab.java +++ b/src/main/java/org/jabref/gui/preferences/journals/JournalAbbreviationsTab.java @@ -4,6 +4,7 @@ import javafx.animation.KeyFrame; import javafx.animation.KeyValue; import javafx.animation.Timeline; +import javafx.application.Platform; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleStringProperty; @@ -11,14 +12,18 @@ import javafx.collections.transformation.FilteredList; import javafx.event.ActionEvent; import javafx.fxml.FXML; +import javafx.scene.Node; import javafx.scene.control.Button; import javafx.scene.control.CheckBox; import javafx.scene.control.ComboBox; import javafx.scene.control.Label; +import javafx.scene.control.ListCell; import javafx.scene.control.ProgressIndicator; import javafx.scene.control.TableColumn; import javafx.scene.control.TableView; +import javafx.scene.control.Tooltip; import javafx.scene.control.cell.TextFieldTableCell; +import javafx.scene.layout.HBox; import javafx.scene.paint.Color; import javafx.util.Duration; @@ -42,6 +47,9 @@ */ public class JournalAbbreviationsTab extends AbstractPreferenceTabView<JournalAbbreviationsTabViewModel> implements PreferencesTab { + private static final String ENABLED_SYMBOL = "✓ "; + private static final String DISABLED_SYMBOL = "○ "; + @FXML private Label loadingLabel; @FXML private ProgressIndicator progressIndicator; @@ -82,6 +90,7 @@ private void initialize() { filteredAbbreviations = new FilteredList<>(viewModel.abbreviationsProperty()); setUpTable(); + setUpToggleButton(); setBindings(); setAnimations(); @@ -110,6 +119,37 @@ private void setUpTable() { .install(actionsColumn); } + /** + * Sets up the toggle button that allows enabling/disabling journal abbreviation lists. + * This method creates a button with appropriate styling and tooltip, then adds it + * to the UI next to the journal files dropdown. When clicked, the button toggles + * the enabled state of the currently selected abbreviation list. + */ + private void setUpToggleButton() { + Button toggleButton = new Button(Localization.lang("Toggle")); + toggleButton.setOnAction(e -> toggleEnableList()); + toggleButton.setTooltip(new Tooltip(Localization.lang("Toggle selected list on/off"))); + toggleButton.getStyleClass().add("icon-button"); + + for (Node node : getChildren()) { + if (node instanceof HBox hbox) { + boolean containsComboBox = false; + for (Node child : hbox.getChildren()) { + if (child == journalFilesBox) { + containsComboBox = true; + break; + } + } + + if (containsComboBox) { + int comboBoxIndex = hbox.getChildren().indexOf(journalFilesBox); + hbox.getChildren().add(comboBoxIndex + 1, toggleButton); + break; + } + } + } + } + private void setBindings() { journalAbbreviationsTable.setItems(filteredAbbreviations); @@ -125,6 +165,20 @@ private void setBindings() { removeAbbreviationListButton.disableProperty().bind(viewModel.isFileRemovableProperty().not()); journalFilesBox.itemsProperty().bindBidirectional(viewModel.journalFilesProperty()); journalFilesBox.valueProperty().bindBidirectional(viewModel.currentFileProperty()); + + journalFilesBox.setCellFactory(listView -> new JournalFileListCell()); + journalFilesBox.setButtonCell(new JournalFileListCell()); + + viewModel.journalFilesProperty().addListener((_, _, newValue) -> { + if (newValue == null) { + return; + } + for (AbbreviationsFileViewModel fileViewModel : newValue) { + fileViewModel.enabledProperty().addListener((_, _, _) -> { + refreshComboBoxDisplay(); + }); + } + }); addAbbreviationButton.disableProperty().bind(viewModel.isEditableAndRemovableProperty().not()); @@ -137,20 +191,48 @@ private void setBindings() { useFJournal.selectedProperty().bindBidirectional(viewModel.useFJournalProperty()); } - private void setAnimations() { - ObjectProperty<Color> flashingColor = new SimpleObjectProperty<>(Color.TRANSPARENT); - StringProperty flashingColorStringProperty = ColorUtil.createFlashingColorStringProperty(flashingColor); - - searchBox.styleProperty().bind( - new SimpleStringProperty("-fx-control-inner-background: ").concat(flashingColorStringProperty).concat(";") - ); - invalidateSearch = new Timeline( - new KeyFrame(Duration.seconds(0), new KeyValue(flashingColor, Color.TRANSPARENT, Interpolator.LINEAR)), - new KeyFrame(Duration.seconds(0.25), new KeyValue(flashingColor, Color.RED, Interpolator.LINEAR)), - new KeyFrame(Duration.seconds(0.25), new KeyValue(searchBox.textProperty(), "", Interpolator.DISCRETE)), - new KeyFrame(Duration.seconds(0.25), (ActionEvent event) -> addAbbreviationActions()), - new KeyFrame(Duration.seconds(0.5), new KeyValue(flashingColor, Color.TRANSPARENT, Interpolator.LINEAR)) - ); + /** + * Custom ListCell to display the journal file items with checkboxes. + * This simply shows the checkbox status without trying to handle + * direct checkbox interactions, to avoid conflicts with ComboBox selection. + */ + private static class JournalFileListCell extends ListCell<AbbreviationsFileViewModel> { + @Override + protected void updateItem(AbbreviationsFileViewModel item, boolean empty) { + super.updateItem(item, empty); + + if (empty || item == null) { + setText(null); + setGraphic(null); + } else { + String prefix = item.isEnabled() ? ENABLED_SYMBOL : DISABLED_SYMBOL; + setText(prefix + item.toString()); + + item.enabledProperty().addListener((obs, oldVal, newVal) -> { + if (newVal != null) { + setText((newVal ? ENABLED_SYMBOL : DISABLED_SYMBOL) + item.toString()); + } + }); + } + } + } + + /** + * Force the ComboBox to refresh its display + */ + private void refreshComboBoxDisplay() { + Platform.runLater(() -> { + AbbreviationsFileViewModel currentSelection = journalFilesBox.getValue(); + + journalFilesBox.setButtonCell(new JournalFileListCell()); + + journalFilesBox.setValue(null); + journalFilesBox.setValue(currentSelection); + + journalFilesBox.setCellFactory(listView -> new JournalFileListCell()); + + journalFilesBox.requestLayout(); + }); } @FXML @@ -201,4 +283,48 @@ private void selectNewAbbreviation() { public String getTabName() { return Localization.lang("Journal abbreviations"); } + + /** + * Toggles the enabled state of the currently selected journal abbreviation list. + * This method performs several important operations: + * <ul> + * <li>Toggles the enabled state of the selected list in the UI</li> + * <li>Refreshes the ComboBox display to show the updated state</li> + * <li>Updates the JournalAbbreviationPreferences to persist this change</li> + * <li>Reloads the entire JournalAbbreviationRepository with the new settings</li> + * <li>Updates the dependency injection container with the new repository</li> + * <li>Marks the view model as dirty to ensure changes are saved</li> + * </ul> + * This is called when the user clicks the toggle button next to the journal files dropdown. + */ + @FXML + private void toggleEnableList() { + AbbreviationsFileViewModel selected = journalFilesBox.getValue(); + if (selected == null) { + return; + } + + boolean newEnabledState = !selected.isEnabled(); + selected.setEnabled(newEnabledState); + + refreshComboBoxDisplay(); + + viewModel.markAsDirty(); + } + + private void setAnimations() { + ObjectProperty<Color> flashingColor = new SimpleObjectProperty<>(Color.TRANSPARENT); + StringProperty flashingColorStringProperty = ColorUtil.createFlashingColorStringProperty(flashingColor); + + searchBox.styleProperty().bind( + new SimpleStringProperty("-fx-control-inner-background: ").concat(flashingColorStringProperty).concat(";") + ); + invalidateSearch = new Timeline( + new KeyFrame(Duration.seconds(0), new KeyValue(flashingColor, Color.TRANSPARENT, Interpolator.LINEAR)), + new KeyFrame(Duration.seconds(0.25), new KeyValue(flashingColor, Color.RED, Interpolator.LINEAR)), + new KeyFrame(Duration.seconds(0.25), new KeyValue(searchBox.textProperty(), "", Interpolator.DISCRETE)), + new KeyFrame(Duration.seconds(0.25), (ActionEvent event) -> addAbbreviationActions()), + new KeyFrame(Duration.seconds(0.5), new KeyValue(flashingColor, Color.TRANSPARENT, Interpolator.LINEAR)) + ); + } } 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..9b5077b1501 100644 --- a/src/main/java/org/jabref/gui/preferences/journals/JournalAbbreviationsTabViewModel.java +++ b/src/main/java/org/jabref/gui/preferences/journals/JournalAbbreviationsTabViewModel.java @@ -4,7 +4,6 @@ import java.nio.file.Path; import java.util.List; import java.util.Objects; -import java.util.stream.Collectors; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleIntegerProperty; @@ -78,13 +77,8 @@ public JournalAbbreviationsTabViewModel(JournalAbbreviationPreferences abbreviat abbreviations.unbindBidirectional(oldValue.abbreviationsProperty()); currentAbbreviation.set(null); } - if (newValue != null) { - isFileRemovable.set(!newValue.isBuiltInListProperty().get()); - abbreviations.bindBidirectional(newValue.abbreviationsProperty()); - if (!abbreviations.isEmpty()) { - currentAbbreviation.set(abbreviations.getLast()); - } - } else { + + if (newValue == null) { isFileRemovable.set(false); if (journalFiles.isEmpty()) { currentAbbreviation.set(null); @@ -92,6 +86,17 @@ public JournalAbbreviationsTabViewModel(JournalAbbreviationPreferences abbreviat } else { currentFile.set(journalFiles.getFirst()); } + return; + } + + isFileRemovable.set(!newValue.isBuiltInListProperty().get()); + if (newValue.abbreviationsProperty() == null) { + return; + } + + abbreviations.bindBidirectional(newValue.abbreviationsProperty()); + if (!abbreviations.isEmpty()) { + currentAbbreviation.set(abbreviations.getLast()); } }); journalFiles.addListener((ListChangeListener<AbbreviationsFileViewModel>) lcl -> { @@ -144,8 +149,13 @@ public void addBuiltInList() { isLoading.setValue(false); List<AbbreviationViewModel> builtInViewModels = result.stream() .map(AbbreviationViewModel::new) - .collect(Collectors.toList()); - journalFiles.add(new AbbreviationsFileViewModel(builtInViewModels, Localization.lang("JabRef built in list"))); + .toList(); + AbbreviationsFileViewModel builtInListModel = new AbbreviationsFileViewModel(builtInViewModels, Localization.lang("JabRef built in list")); + + boolean isEnabled = abbreviationsPreferences.isSourceEnabled(JournalAbbreviationRepository.BUILTIN_LIST_ID); + builtInListModel.setEnabled(isEnabled); + + journalFiles.add(builtInListModel); selectLastJournalFile(); }) .onFailure(dialogService::showErrorDialogAndWait) @@ -181,6 +191,10 @@ private void openFile(Path filePath) { if (abbreviationsFile.exists()) { try { abbreviationsFile.readAbbreviations(); + + String fileName = filePath.getFileName().toString(); + boolean isEnabled = abbreviationsPreferences.isSourceEnabled(fileName); + abbreviationsFile.setEnabled(isEnabled); } catch (IOException e) { LOGGER.debug("Could not read abbreviations file", e); } @@ -203,11 +217,13 @@ public void openFile() { * except if there are no more files than the {@code activeFile} property will be set to {@code null}. */ public void removeCurrentFile() { - if (isFileRemovable.get()) { - journalFiles.remove(currentFile.get()); - if (journalFiles.isEmpty()) { - currentFile.set(null); - } + if (!isFileRemovable.get()) { + return; + } + + journalFiles.remove(currentFile.get()); + if (journalFiles.isEmpty()) { + currentFile.set(null); } } @@ -238,18 +254,20 @@ public void addAbbreviation() { * Method to change the currentAbbreviation property to a new abbreviation. */ void editAbbreviation(Abbreviation abbreviationObject) { - if (isEditableAndRemovable.get()) { - AbbreviationViewModel abbViewModel = new AbbreviationViewModel(abbreviationObject); - if (abbreviations.contains(abbViewModel)) { - if (abbViewModel.equals(currentAbbreviation.get())) { - setCurrentAbbreviationNameAndAbbreviationIfValid(abbreviationObject); - } else { - dialogService.showErrorDialogAndWait(Localization.lang("Duplicated Journal Abbreviation"), - Localization.lang("Abbreviation '%0' for journal '%1' already defined.", abbreviationObject.getAbbreviation(), abbreviationObject.getName())); - } - } else { + if (!isEditableAndRemovable.get()) { + return; + } + + AbbreviationViewModel abbViewModel = new AbbreviationViewModel(abbreviationObject); + if (abbreviations.contains(abbViewModel)) { + if (abbViewModel.equals(currentAbbreviation.get())) { setCurrentAbbreviationNameAndAbbreviationIfValid(abbreviationObject); + } else { + dialogService.showErrorDialogAndWait(Localization.lang("Duplicated Journal Abbreviation"), + Localization.lang("Abbreviation '%0' for journal '%1' already defined.", abbreviationObject.getAbbreviation(), abbreviationObject.getName())); } + } else { + setCurrentAbbreviationNameAndAbbreviationIfValid(abbreviationObject); } } @@ -279,18 +297,20 @@ private void setCurrentAbbreviationNameAndAbbreviationIfValid(Abbreviation abbre * {@code null} if there are no abbreviations left. */ public void deleteAbbreviation() { - if ((currentAbbreviation.get() != null) && !currentAbbreviation.get().isPseudoAbbreviation()) { - int index = abbreviations.indexOf(currentAbbreviation.get()); - if (index > 1) { - currentAbbreviation.set(abbreviations.get(index - 1)); - } else if ((index + 1) < abbreviationsCount.get()) { - currentAbbreviation.set(abbreviations.get(index + 1)); - } else { - currentAbbreviation.set(null); - } - abbreviations.remove(index); - shouldWriteLists = true; + if ((currentAbbreviation.get() == null) || currentAbbreviation.get().isPseudoAbbreviation()) { + return; } + + int index = abbreviations.indexOf(currentAbbreviation.get()); + if (index > 1) { + currentAbbreviation.set(abbreviations.get(index - 1)); + } else if ((index + 1) < abbreviationsCount.get()) { + currentAbbreviation.set(abbreviations.get(index + 1)); + } else { + currentAbbreviation.set(null); + } + abbreviations.remove(index); + shouldWriteLists = true; } public void removeAbbreviation(AbbreviationViewModel abbreviation) { @@ -331,23 +351,43 @@ public void storeSettings() { .filter(path -> !path.isBuiltInListProperty().get()) .filter(path -> path.getAbsolutePath().isPresent()) .map(path -> path.getAbsolutePath().get().toAbsolutePath().toString()) - .collect(Collectors.toList()); + .toList(); abbreviationsPreferences.setExternalJournalLists(journalStringList); abbreviationsPreferences.setUseFJournalField(useFJournal.get()); + + for (AbbreviationsFileViewModel fileViewModel : journalFiles) { + if (fileViewModel.isBuiltInListProperty().get()) { + abbreviationsPreferences.setSourceEnabled(JournalAbbreviationRepository.BUILTIN_LIST_ID, fileViewModel.isEnabled()); + } else if (fileViewModel.getAbsolutePath().isPresent()) { + String fileName = fileViewModel.getAbsolutePath().get().getFileName().toString(); + abbreviationsPreferences.setSourceEnabled(fileName, fileViewModel.isEnabled()); + } + } if (shouldWriteLists) { saveJournalAbbreviationFiles(); shouldWriteLists = false; } }) - .onSuccess(success -> Injector.setModelOrService( - JournalAbbreviationRepository.class, - JournalAbbreviationLoader.loadRepository(abbreviationsPreferences))) - .onFailure(exception -> LOGGER.error("Failed to store journal preferences.", exception)) + .onSuccess(result -> { + JournalAbbreviationRepository newRepository = + JournalAbbreviationLoader.loadRepository(abbreviationsPreferences); + + Injector.setModelOrService(JournalAbbreviationRepository.class, newRepository); + }) + .onFailure(exception -> LOGGER.error("Failed to store journal preferences", exception)) .executeWith(taskExecutor); } + /** + * Marks the abbreviation lists as needing to be saved + * This only tracks the dirty state but doesn't write to preferences + */ + public void markAsDirty() { + shouldWriteLists = true; + } + public SimpleBooleanProperty isLoadingProperty() { return isLoading; } diff --git a/src/main/java/org/jabref/logic/journals/AbbreviationParser.java b/src/main/java/org/jabref/logic/journals/AbbreviationParser.java index 291016c1259..131b085a9bc 100644 --- a/src/main/java/org/jabref/logic/journals/AbbreviationParser.java +++ b/src/main/java/org/jabref/logic/journals/AbbreviationParser.java @@ -6,8 +6,8 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.Arrays; -import java.util.Collection; import java.util.LinkedHashSet; +import java.util.Set; import org.apache.commons.csv.CSVParser; import org.apache.commons.csv.CSVRecord; @@ -63,7 +63,7 @@ private char detectDelimiter(Path file) throws IOException { } } - public Collection<Abbreviation> getAbbreviations() { + public Set<Abbreviation> getAbbreviations() { return abbreviations; } } diff --git a/src/main/java/org/jabref/logic/journals/JournalAbbreviationLoader.java b/src/main/java/org/jabref/logic/journals/JournalAbbreviationLoader.java index 0d0b7d665c0..7f423b9ef76 100644 --- a/src/main/java/org/jabref/logic/journals/JournalAbbreviationLoader.java +++ b/src/main/java/org/jabref/logic/journals/JournalAbbreviationLoader.java @@ -5,9 +5,9 @@ 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 java.util.Set; import org.jabref.logic.journals.ltwa.LtwaRepository; @@ -26,27 +26,47 @@ public class JournalAbbreviationLoader { private static final Logger LOGGER = LoggerFactory.getLogger(JournalAbbreviationLoader.class); + private static final boolean USE_FJOURNAL_FIELD = true; + private static final boolean BUILTIN_LIST_ENABLED_BY_DEFAULT = true; - public static Collection<Abbreviation> readAbbreviationsFromCsvFile(Path file) throws IOException { + /** + * Reads journal abbreviations from a CSV file. + * + * @param file Path to the CSV file containing journal abbreviations + * @return A set of abbreviations read from the file + * @throws IOException If an I/O error occurs while reading the file + */ + public static Set<Abbreviation> readAbbreviationsFromCsvFile(Path file) throws IOException { LOGGER.debug("Reading journal list from file {}", file); AbbreviationParser parser = new AbbreviationParser(); parser.readJournalListFromFile(file); return parser.getAbbreviations(); } + /** + * Loads a journal abbreviation repository based on the given preferences. + * Takes into account enabled/disabled state of journal abbreviation sources. + * + * @param journalAbbreviationPreferences The preferences containing journal list paths and enabled states + * @return A repository with loaded abbreviations + */ public static JournalAbbreviationRepository loadRepository(JournalAbbreviationPreferences journalAbbreviationPreferences) { JournalAbbreviationRepository repository; + boolean builtInEnabled = journalAbbreviationPreferences.isSourceEnabled(JournalAbbreviationRepository.BUILTIN_LIST_ID); + // Initialize with built-in list try (InputStream resourceAsStream = JournalAbbreviationRepository.class.getResourceAsStream("/journals/journal-list.mv")) { if (resourceAsStream == null) { LOGGER.warn("There is no journal-list.mv. We use a default journal list"); repository = new JournalAbbreviationRepository(); + repository.setSourceEnabled(JournalAbbreviationRepository.BUILTIN_LIST_ID, builtInEnabled); } else { Path tempDir = Files.createTempDirectory("jabref-journal"); Path tempJournalList = tempDir.resolve("journal-list.mv"); Files.copy(resourceAsStream, tempJournalList); repository = new JournalAbbreviationRepository(tempJournalList, loadLtwaRepository()); + repository.setSourceEnabled(JournalAbbreviationRepository.BUILTIN_LIST_ID, builtInEnabled); tempDir.toFile().deleteOnExit(); tempJournalList.toFile().deleteOnExit(); } @@ -63,7 +83,37 @@ public static JournalAbbreviationRepository loadRepository(JournalAbbreviationPr Collections.reverse(lists); for (String filename : lists) { try { - repository.addCustomAbbreviations(readAbbreviationsFromCsvFile(Path.of(filename))); + Path filePath = Path.of(filename); + + String simpleFilename = filePath.getFileName().toString(); + String prefixedKey = "JABREF_JOURNAL_LIST:" + simpleFilename; + String absolutePath = filePath.toAbsolutePath().toString(); + + boolean enabled = false; + + if (journalAbbreviationPreferences.hasExplicitEnabledSetting(prefixedKey)) { + enabled = journalAbbreviationPreferences.isSourceEnabled(prefixedKey); + LOGGER.debug("Loading external abbreviations file {} (found by prefixed key): enabled = {}", + simpleFilename, enabled); + } else if (journalAbbreviationPreferences.hasExplicitEnabledSetting(simpleFilename)) { + enabled = journalAbbreviationPreferences.isSourceEnabled(simpleFilename); + LOGGER.debug("Loading external abbreviations file {} (found by filename): enabled = {}", + simpleFilename, enabled); + } else if (journalAbbreviationPreferences.hasExplicitEnabledSetting(absolutePath)) { + enabled = journalAbbreviationPreferences.isSourceEnabled(absolutePath); + LOGGER.debug("Loading external abbreviations file {} (found by path): enabled = {}", + simpleFilename, enabled); + } else { + enabled = true; + LOGGER.debug("Loading external abbreviations file {} (no settings found): enabled = {}", + simpleFilename, enabled); + } + + Set<Abbreviation> abbreviations = readAbbreviationsFromCsvFile(filePath); + + repository.addCustomAbbreviations(abbreviations, simpleFilename, enabled); + + repository.addCustomAbbreviations(Set.of(), prefixedKey, enabled); } catch (IOException | InvalidPathException e) { // invalid path might come from unix/windows mixup of prefs LOGGER.error("Cannot read external journal list file {}", filename, e); @@ -91,6 +141,9 @@ private static LtwaRepository loadLtwaRepository() throws IOException { } public static JournalAbbreviationRepository loadBuiltInRepository() { - return loadRepository(new JournalAbbreviationPreferences(Collections.emptyList(), true)); + JournalAbbreviationPreferences prefs = new JournalAbbreviationPreferences(List.of(), USE_FJOURNAL_FIELD); + + prefs.setSourceEnabled(JournalAbbreviationRepository.BUILTIN_LIST_ID, BUILTIN_LIST_ENABLED_BY_DEFAULT); + return loadRepository(prefs); } } diff --git a/src/main/java/org/jabref/logic/journals/JournalAbbreviationPreferences.java b/src/main/java/org/jabref/logic/journals/JournalAbbreviationPreferences.java index fb256bbe8ab..adb3af964e8 100644 --- a/src/main/java/org/jabref/logic/journals/JournalAbbreviationPreferences.java +++ b/src/main/java/org/jabref/logic/journals/JournalAbbreviationPreferences.java @@ -1,23 +1,62 @@ package org.jabref.logic.journals; +import java.util.HashMap; import java.util.List; +import java.util.Map; import javafx.beans.property.BooleanProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.collections.FXCollections; import javafx.collections.ObservableList; +import javafx.collections.ObservableMap; +/** + * Class for storing and managing journal abbreviation preferences + */ public class JournalAbbreviationPreferences { + public static final String ENABLED_EXTERNAL_JOURNAL_LISTS = "enabledExternalJournalLists"; + private final ObservableList<String> externalJournalLists; private final BooleanProperty useFJournalField; - + private final ObservableMap<String, Boolean> enabledExternalLists = FXCollections.observableHashMap(); + + // We use a separate property for change tracking because ObservableMap listeners + // do not always fire correctly when used for UI bindings, especially when + // clearing and re-adding multiple entries at once. + // The actual boolean value does not matter because we toggle it (true → false or false → true) + // to signal to listeners that a change occurred. This technique is a "dirty flag". + private final BooleanProperty enabledListsChanged = new SimpleBooleanProperty(); + + /** + * Constructs a new JournalAbbreviationPreferences with the given external journal lists and FJournal field preference + * + * @param externalJournalLists List of paths to external journal abbreviation files + * @param useFJournalField Whether to use the FJournal field + */ public JournalAbbreviationPreferences(List<String> externalJournalLists, boolean useFJournalField) { this.externalJournalLists = FXCollections.observableArrayList(externalJournalLists); this.useFJournalField = new SimpleBooleanProperty(useFJournalField); } + /** + * Constructs a new JournalAbbreviationPreferences with the given external journal lists, FJournal field preference, + * and enabled states for journal abbreviation sources + * + * @param externalJournalLists List of paths to external journal abbreviation files + * @param useFJournalField Whether to use the FJournal field + * @param enabledExternalLists Map of source paths to their enabled states + */ + public JournalAbbreviationPreferences(List<String> externalJournalLists, + boolean useFJournalField, + Map<String, Boolean> enabledExternalLists) { + this(externalJournalLists, useFJournalField); + if (enabledExternalLists != null) { + this.enabledExternalLists.putAll(enabledExternalLists); + } + } + public ObservableList<String> getExternalJournalLists() { return externalJournalLists; } @@ -38,4 +77,100 @@ public BooleanProperty useFJournalFieldProperty() { public void setUseFJournalField(boolean useFJournalField) { this.useFJournalField.set(useFJournalField); } + + /** + * Checks if a journal abbreviation source is enabled + * + * @param sourcePath Path to the abbreviation source + * @return true if the source is enabled or has no explicit state (default is enabled) + */ + public boolean isSourceEnabled(String sourcePath) { + return enabledExternalLists.getOrDefault(sourcePath, true); + } + + /** + * Sets the enabled state for a journal abbreviation source + * + * @param sourcePath Path to the abbreviation source + * @param enabled Whether the source should be enabled + */ + public void setSourceEnabled(String sourcePath, boolean enabled) { + enabledExternalLists.put(sourcePath, enabled); + notifyChange(); + } + + /** + * Notifies listeners that the enabled states have changed. + * This simply toggles the boolean value to trigger listeners. + */ + private void notifyChange() { + enabledListsChanged.set(!enabledListsChanged.get()); + } + + /** + * Gets all enabled/disabled states for journal abbreviation sources + * + * @return Map of source paths to their enabled states + */ + public Map<String, Boolean> getEnabledExternalLists() { + return new HashMap<>(enabledExternalLists); + } + + /** + * Sets all enabled/disabled states for journal abbreviation sources + * + * @param enabledLists Map of source paths to their enabled states + */ + public void setEnabledExternalLists(Map<String, Boolean> enabledLists) { + this.enabledExternalLists.clear(); + if (enabledLists != null) { + this.enabledExternalLists.putAll(enabledLists); + } + notifyChange(); + } + + /** + * Property that changes whenever the enabled states map changes + * Used for binding/listening to changes + * + * @return A boolean property that toggles when enabled states change + */ + public BooleanProperty enabledListsChangedProperty() { + return enabledListsChanged; + } + + /** + * Checks if a specific source has an explicit enabled/disabled setting + * + * @param sourcePath Path to check + * @return True if there is an explicit setting for this source + */ + public boolean hasExplicitEnabledSetting(String sourcePath) { + return enabledExternalLists.containsKey(sourcePath); + } + + /** + * Checks if any journal abbreviation source is enabled in the preferences. + * This includes both the built-in list and any external journal lists. + * + * @return true if at least one source is enabled, false if all sources are disabled + */ + public boolean areAnyJournalSourcesEnabled() { + boolean anySourceEnabled = isSourceEnabled(JournalAbbreviationRepository.BUILTIN_LIST_ID); + + if (!anySourceEnabled) { + for (String listPath : getExternalJournalLists()) { + if (listPath != null && !listPath.isBlank()) { + // Just check the filename since that's what's used as the source key + String fileName = java.nio.file.Path.of(listPath).getFileName().toString(); + if (isSourceEnabled(fileName)) { + anySourceEnabled = true; + break; + } + } + } + } + + return anySourceEnabled; + } } diff --git a/src/main/java/org/jabref/logic/journals/JournalAbbreviationRepository.java b/src/main/java/org/jabref/logic/journals/JournalAbbreviationRepository.java index 3af86c93a5e..fbbb14e33c2 100644 --- a/src/main/java/org/jabref/logic/journals/JournalAbbreviationRepository.java +++ b/src/main/java/org/jabref/logic/journals/JournalAbbreviationRepository.java @@ -1,6 +1,7 @@ package org.jabref.logic.journals; import java.nio.file.Path; +import java.util.ArrayList; import java.util.Collection; import java.util.Comparator; import java.util.HashMap; @@ -18,11 +19,17 @@ import org.h2.mvstore.MVMap; import org.h2.mvstore.MVStore; +import org.jspecify.annotations.NonNull; /** * A repository for all journal abbreviations, including add and find methods. */ public class JournalAbbreviationRepository { + /** + * Identifier for the built-in abbreviation list + */ + public static final String BUILTIN_LIST_ID = "BUILTIN_LIST"; + static final Pattern QUESTION_MARK = Pattern.compile("\\?"); private final Map<String, Abbreviation> fullToAbbreviationObject = new HashMap<>(); @@ -32,6 +39,8 @@ public class JournalAbbreviationRepository { private final TreeSet<Abbreviation> customAbbreviations = new TreeSet<>(); private final StringSimilarity similarity = new StringSimilarity(); private final LtwaRepository ltwaRepository; + private final Map<String, Boolean> enabledSources = new HashMap<>(); + private final Map<Abbreviation, String> abbreviationSources = new HashMap<>(); /** * Initializes the internal data based on the abbreviations found in the given MV file @@ -55,9 +64,12 @@ public JournalAbbreviationRepository(Path journalList, LtwaRepository ltwaReposi abbreviationToAbbreviationObject.put(abbrevationString, newAbbreviation); dotlessToAbbreviationObject.put(newAbbreviation.getDotlessAbbreviation(), newAbbreviation); shortestUniqueToAbbreviationObject.put(shortestUniqueAbbreviation, newAbbreviation); + + abbreviationSources.put(newAbbreviation, BUILTIN_LIST_ID); }); } this.ltwaRepository = ltwaRepository; + enabledSources.put(BUILTIN_LIST_ID, true); } /** @@ -73,6 +85,9 @@ public JournalAbbreviationRepository() { abbreviationToAbbreviationObject.put("Demo", newAbbreviation); dotlessToAbbreviationObject.put("Demo", newAbbreviation); shortestUniqueToAbbreviationObject.put("Dem", newAbbreviation); + + abbreviationSources.put(newAbbreviation, BUILTIN_LIST_ID); + enabledSources.put(BUILTIN_LIST_ID, true); ltwaRepository = new LtwaRepository(); } @@ -94,80 +109,129 @@ private static boolean isMatchedAbbreviated(String name, Abbreviation abbreviati } /** - * Returns true if the given journal name is contained in the list either in its full form - * (e.g., Physical Review Letters) or its abbreviated form (e.g., Phys. Rev. Lett.). - * If the exact match is not found, attempts a fuzzy match to recognize minor input errors. + * Returns true if the given journal name is in its abbreviated form (e.g. Phys. Rev. Lett.). The test is strict, + * i.e., journals whose abbreviation is the same as the full name are not considered. + * Respects the enabled/disabled state of abbreviation sources. */ - public boolean isKnownName(String journalName) { + public boolean isAbbreviatedName(String journalName) { if (QUESTION_MARK.matcher(journalName).find()) { return false; } - return get(journalName).isPresent(); - } - - /** - * Get the LTWA abbreviation for the given journal name. - */ - public Optional<String> getLtwaAbbreviation(String journalName) { - if (QUESTION_MARK.matcher(journalName).find()) { - return Optional.of(journalName); + String journal = journalName.trim().replaceAll(Matcher.quoteReplacement("\\&"), "&"); + + boolean isCustomAbbreviated = customAbbreviations.stream() + .filter(abbreviation -> isSourceEnabled(abbreviationSources.getOrDefault(abbreviation, BUILTIN_LIST_ID))) + .anyMatch(abbreviation -> isMatchedAbbreviated(journal, abbreviation)); + + boolean builtInEnabled = isSourceEnabled(BUILTIN_LIST_ID); + if (!builtInEnabled) { + return isCustomAbbreviated; } - return ltwaRepository.abbreviate(journalName); + + boolean inAbbreviationMap = abbreviationToAbbreviationObject.containsKey(journal); + boolean inDotlessMap = dotlessToAbbreviationObject.containsKey(journal); + boolean inShortestMap = shortestUniqueToAbbreviationObject.containsKey(journal); + + boolean isAbbreviated = isCustomAbbreviated || inAbbreviationMap || inDotlessMap || inShortestMap; + + return isAbbreviated; } /** - * Returns true if the given journal name is in its abbreviated form (e.g. Phys. Rev. Lett.). The test is strict, - * i.e., journals whose abbreviation is the same as the full name are not considered + * Returns true if the given journal name is contained in the list either in its full form + * (e.g., Physical Review Letters) or its abbreviated form (e.g., Phys. Rev. Lett.). + * If the exact match is not found, attempts a fuzzy match to recognize minor input errors. + * Respects the enabled/disabled state of abbreviation sources. */ - public boolean isAbbreviatedName(String journalName) { + public boolean isKnownName(String journalName) { if (QUESTION_MARK.matcher(journalName).find()) { return false; } - String journal = journalName.trim().replaceAll(Matcher.quoteReplacement("\\&"), "&"); - return customAbbreviations.stream().anyMatch(abbreviation -> isMatchedAbbreviated(journal, abbreviation)) - || abbreviationToAbbreviationObject.containsKey(journal) - || dotlessToAbbreviationObject.containsKey(journal) - || shortestUniqueToAbbreviationObject.containsKey(journal); + return get(journalName).isPresent(); } /** * Attempts to get the abbreviation of the journal given. * if no exact match is found, attempts a fuzzy match on full journal names. + * This method respects the enabled/disabled state of journal abbreviation sources. * * @param input The journal name (either full name or abbreviated name). */ public Optional<Abbreviation> get(String input) { // Clean up input: trim and unescape ampersand String journal = input.trim().replaceAll(Matcher.quoteReplacement("\\&"), "&"); - + Optional<Abbreviation> customAbbreviation = customAbbreviations.stream() - .filter(abbreviation -> isMatched(journal, abbreviation)) - .findFirst(); + .filter(abbreviation -> isSourceEnabled(abbreviationSources.getOrDefault(abbreviation, BUILTIN_LIST_ID))) + .filter(abbreviation -> isMatched(journal, abbreviation)) + .findFirst(); + if (customAbbreviation.isPresent()) { return customAbbreviation; } - Optional<Abbreviation> abbreviation = Optional.ofNullable(fullToAbbreviationObject.get(journal)) + if (!isSourceEnabled(BUILTIN_LIST_ID)) { + return Optional.empty(); + } + + Optional<Abbreviation> builtInAbbreviation = Optional.ofNullable(fullToAbbreviationObject.get(journal)) .or(() -> Optional.ofNullable(abbreviationToAbbreviationObject.get(journal))) .or(() -> Optional.ofNullable(dotlessToAbbreviationObject.get(journal))) .or(() -> Optional.ofNullable(shortestUniqueToAbbreviationObject.get(journal))); - if (abbreviation.isEmpty()) { - abbreviation = findAbbreviationFuzzyMatched(journal); + if (builtInAbbreviation.isPresent()) { + return builtInAbbreviation; } - return abbreviation; + return findAbbreviationFuzzyMatched(journal); + } + + /** + * Specialized method for unabbreviation that first checks if a journal name is abbreviated + * from an enabled source before attempting to find its full form. + * + * @param input The journal name to check and unabbreviate. + * @return Optional containing the abbreviation object if the input is an abbreviation + * from an enabled source, empty otherwise. + */ + public Optional<Abbreviation> getForUnabbreviation(String input) { + boolean builtInEnabled = isSourceEnabled(BUILTIN_LIST_ID); + + boolean isAbbreviated = isAbbreviatedName(input); + + if (!isAbbreviated) { + return Optional.empty(); + } + + Optional<Abbreviation> result = get(input); + + return result; } private Optional<Abbreviation> findAbbreviationFuzzyMatched(String input) { - Optional<Abbreviation> customMatch = findBestFuzzyMatched(customAbbreviations, input); + List<Abbreviation> enabledCustomAbbreviations = customAbbreviations.stream() + .filter(abbreviation -> isSourceEnabled(abbreviationSources.getOrDefault(abbreviation, BUILTIN_LIST_ID))) + .toList(); + + Optional<Abbreviation> customMatch = findBestFuzzyMatched(enabledCustomAbbreviations, input); if (customMatch.isPresent()) { return customMatch; } + if (!isSourceEnabled(BUILTIN_LIST_ID)) { + return Optional.empty(); + } + return findBestFuzzyMatched(fullToAbbreviationObject.values(), input); } + /** + * Finds the best matching abbreviation by fuzzy matching from a collection of abbreviations. + * + * @param abbreviations Collection of abbreviations to search + * @param input The input string to match against + * @return Optional containing the best matching abbreviation, or empty if no good match is found + */ private Optional<Abbreviation> findBestFuzzyMatched(Collection<Abbreviation> abbreviations, String input) { // threshold for edit distance similarity comparison final double SIMILARITY_THRESHOLD = 1.0; @@ -194,6 +258,11 @@ private Optional<Abbreviation> findBestFuzzyMatched(Collection<Abbreviation> abb return Optional.of(candidates.getFirst()); } + /** + * Adds a journal abbreviation to the list of custom abbreviations. + * + * @param abbreviation The journal abbreviation to add. + */ public void addCustomAbbreviation(Abbreviation abbreviation) { Objects.requireNonNull(abbreviation); @@ -201,16 +270,109 @@ public void addCustomAbbreviation(Abbreviation abbreviation) { // The set automatically "removes" duplicates // What is a duplicate? An abbreviation is NOT the same if any field is NOT equal (e.g., if the shortest unique differs, the abbreviation is NOT the same) customAbbreviations.add(abbreviation); + + abbreviationSources.put(abbreviation, BUILTIN_LIST_ID); + } + + /** + * Adds a custom abbreviation to the repository with source tracking + * + * @param abbreviation The abbreviation to add + * @param sourcePath The path or identifier of the source + * @param enabled Whether the source is enabled + */ + public void addCustomAbbreviation(@NonNull Abbreviation abbreviation, @NonNull String sourcePath, boolean enabled) { + customAbbreviations.add(abbreviation); + abbreviationSources.put(abbreviation, sourcePath); + + enabledSources.put(sourcePath, enabled); } - public Collection<Abbreviation> getCustomAbbreviations() { + public Set<Abbreviation> getCustomAbbreviations() { return customAbbreviations; } - public void addCustomAbbreviations(Collection<Abbreviation> abbreviationsToAdd) { + /** + * Adds multiple custom abbreviations to the repository + * + * @param abbreviationsToAdd The set of abbreviations to add + */ + public void addCustomAbbreviations(Set<Abbreviation> abbreviationsToAdd) { abbreviationsToAdd.forEach(this::addCustomAbbreviation); } + + /** + * Adds abbreviations with a specific source key and enabled state + * + * @param abbreviationsToAdd Set of abbreviations to add + * @param sourceKey The key identifying the source of these abbreviations + * @param enabled Whether the source should be enabled initially + */ + public void addCustomAbbreviations(Set<Abbreviation> abbreviationsToAdd, String sourceKey, boolean enabled) { + enabledSources.put(sourceKey, enabled); + + for (Abbreviation abbreviation : abbreviationsToAdd) { + customAbbreviations.add(abbreviation); + abbreviationSources.put(abbreviation, sourceKey); + } + } + + /** + * Checks if a journal abbreviation source is enabled + * + * @param sourceKey The key identifying the source + * @return true if the source is enabled or has no explicit state (default is enabled) + */ + public boolean isSourceEnabled(String sourceKey) { + return enabledSources.getOrDefault(sourceKey, true); + } + + /** + * Sets the enabled state for a journal abbreviation source + * + * @param sourceKey The key identifying the source + * @param enabled Whether the source should be enabled + */ + public void setSourceEnabled(String sourceKey, boolean enabled) { + enabledSources.put(sourceKey, enabled); + } + /** + * Specialized method for abbreviation that checks if a journal name is already in its + * full form and requires abbreviation from an enabled source. + * + * @param input The journal name to check and abbreviate. + * @return Optional containing the abbreviation object if the input is a full journal name + * from an enabled source, empty otherwise. + */ + public Optional<Abbreviation> getForAbbreviation(String input) { + String journal = input.trim().replaceAll(Matcher.quoteReplacement("\\&"), "&"); + + Optional<Abbreviation> customAbbreviation = customAbbreviations.stream() + .filter(abbreviation -> isSourceEnabled(abbreviationSources.getOrDefault(abbreviation, BUILTIN_LIST_ID))) + .filter(abbreviation -> isMatched(journal, abbreviation)) + .findFirst(); + + if (customAbbreviation.isPresent()) { + return customAbbreviation; + } + + if (!isSourceEnabled(BUILTIN_LIST_ID)) { + return Optional.empty(); + } + + Optional<Abbreviation> builtInAbbreviation = Optional.ofNullable(fullToAbbreviationObject.get(journal)) + .or(() -> Optional.ofNullable(abbreviationToAbbreviationObject.get(journal))) + .or(() -> Optional.ofNullable(dotlessToAbbreviationObject.get(journal))) + .or(() -> Optional.ofNullable(shortestUniqueToAbbreviationObject.get(journal))); + + if (builtInAbbreviation.isPresent()) { + return builtInAbbreviation; + } + + return findAbbreviationFuzzyMatched(journal); + } + public Optional<String> getNextAbbreviation(String text) { return get(text).map(abbreviation -> abbreviation.getNext(text)); } @@ -231,7 +393,79 @@ public Set<String> getFullNames() { return fullToAbbreviationObject.keySet(); } - public Collection<Abbreviation> getAllLoaded() { - return fullToAbbreviationObject.values(); + public Set<Abbreviation> getAllLoaded() { + return Set.copyOf(fullToAbbreviationObject.values()); + } + + /** + * Class that pairs an abbreviation with its source for tracking purposes + */ + public static class AbbreviationWithSource { + private final Abbreviation abbreviation; + private final String source; + + public AbbreviationWithSource(Abbreviation abbreviation, String source) { + this.abbreviation = abbreviation; + this.source = source; + } + + public Abbreviation getAbbreviation() { + return abbreviation; + } + + public String getSource() { + return source; + } + } + + /** + * Gets all abbreviations from both custom and built-in sources with their sources + * + * @return List of all abbreviations with their sources + */ + public List<AbbreviationWithSource> getAllAbbreviationsWithSources() { + List<AbbreviationWithSource> result = new ArrayList<>(); + + for (Abbreviation abbr : customAbbreviations) { + String source = abbreviationSources.getOrDefault(abbr, "UNKNOWN"); + result.add(new AbbreviationWithSource(abbr, source)); + } + + for (Abbreviation abbr : fullToAbbreviationObject.values()) { + result.add(new AbbreviationWithSource(abbr, BUILTIN_LIST_ID)); + } + + return result; + } + + /** + * Gets all abbreviations from both custom and built-in sources + * + * @return List of all abbreviations + */ + public List<Abbreviation> getAllAbbreviations() { + return getAllAbbreviationsWithSources().stream() + .map(AbbreviationWithSource::getAbbreviation) + .toList(); + } + + /** + * Gets the source identifier for a given abbreviation + * + * @param abbreviation The abbreviation to look up + * @return The source key, or BUILTIN_LIST_ID if not found + */ + public String getSourceForAbbreviation(Abbreviation abbreviation) { + return abbreviationSources.getOrDefault(abbreviation, BUILTIN_LIST_ID); + } + + /** + * Get the LTWA abbreviation for the given journal name. + */ + public Optional<String> getLtwaAbbreviation(String journalName) { + if (QUESTION_MARK.matcher(journalName).find()) { + return Optional.of(journalName); + } + return ltwaRepository.abbreviate(journalName); } } diff --git a/src/main/java/org/jabref/logic/preferences/JabRefCliPreferences.java b/src/main/java/org/jabref/logic/preferences/JabRefCliPreferences.java index 7df1c943415..b03c3e59abf 100644 --- a/src/main/java/org/jabref/logic/preferences/JabRefCliPreferences.java +++ b/src/main/java/org/jabref/logic/preferences/JabRefCliPreferences.java @@ -330,6 +330,7 @@ public class JabRefCliPreferences implements CliPreferences { // Journal private static final String EXTERNAL_JOURNAL_LISTS = "externalJournalLists"; + private static final String ENABLED_EXTERNAL_JOURNAL_LISTS = "enabledExternalJournalLists"; private static final String USE_AMS_FJOURNAL = "useAMSFJournal"; // Protected terms @@ -688,6 +689,9 @@ protected JabRefCliPreferences() { // endregion // endregion + + // region:Journal abbreviations + defaults.put(JournalAbbreviationPreferences.ENABLED_EXTERNAL_JOURNAL_LISTS, Boolean.FALSE); } public void setLanguageDependentDefaultValues() { @@ -726,7 +730,7 @@ static String convertListToString(List<String> value) { @VisibleForTesting static List<String> convertStringToList(String toConvert) { if (StringUtil.isBlank(toConvert)) { - return Collections.emptyList(); + return List.of(); } return Splitter.on(STRINGLIST_DELIMITER).splitToList(toConvert); @@ -1025,14 +1029,41 @@ public JournalAbbreviationPreferences getJournalAbbreviationPreferences() { return journalAbbreviationPreferences; } + Map<String, Boolean> enabledExternalLists = new HashMap<>(); + + enabledExternalLists.put(JournalAbbreviationRepository.BUILTIN_LIST_ID, + getBoolean(ENABLED_EXTERNAL_JOURNAL_LISTS + ":" + JournalAbbreviationRepository.BUILTIN_LIST_ID, true)); + + for (String path : getStringList(EXTERNAL_JOURNAL_LISTS)) { + try { + String absolutePath = Path.of(path).toAbsolutePath().toString(); + enabledExternalLists.put(absolutePath, getBoolean(ENABLED_EXTERNAL_JOURNAL_LISTS + ":" + absolutePath, true)); + } catch (Exception e) { + enabledExternalLists.put(path, getBoolean(ENABLED_EXTERNAL_JOURNAL_LISTS + ":" + path, true)); + LOGGER.warn("Could not resolve absolute path for {}", path, e); + } + } + journalAbbreviationPreferences = new JournalAbbreviationPreferences( getStringList(EXTERNAL_JOURNAL_LISTS), - getBoolean(USE_AMS_FJOURNAL)); + getBoolean(USE_AMS_FJOURNAL), + enabledExternalLists); journalAbbreviationPreferences.getExternalJournalLists().addListener((InvalidationListener) change -> putStringList(EXTERNAL_JOURNAL_LISTS, journalAbbreviationPreferences.getExternalJournalLists())); EasyBind.listen(journalAbbreviationPreferences.useFJournalFieldProperty(), (obs, oldValue, newValue) -> putBoolean(USE_AMS_FJOURNAL, newValue)); + + journalAbbreviationPreferences.getEnabledExternalLists().forEach((path, enabled) -> { + LOGGER.debug("Setting preference value for journal list: {} = {}", path, enabled); + putBoolean(ENABLED_EXTERNAL_JOURNAL_LISTS + ":" + path, enabled); + }); + + journalAbbreviationPreferences.enabledListsChangedProperty().addListener(observable -> { + journalAbbreviationPreferences.getEnabledExternalLists().forEach((path, enabled) -> { + putBoolean(ENABLED_EXTERNAL_JOURNAL_LISTS + ":" + path, enabled); + }); + }); return journalAbbreviationPreferences; } @@ -1534,10 +1565,10 @@ public FieldPreferences getFieldPreferences() { !getBoolean(DO_NOT_RESOLVE_STRINGS), // mind the ! getStringList(RESOLVE_STRINGS_FOR_FIELDS).stream() .map(FieldFactory::parseField) - .collect(Collectors.toList()), + .toList(), getStringList(NON_WRAPPABLE_FIELDS).stream() .map(FieldFactory::parseField) - .collect(Collectors.toList())); + .toList()); EasyBind.listen(fieldPreferences.resolveStringsProperty(), (obs, oldValue, newValue) -> putBoolean(DO_NOT_RESOLVE_STRINGS, !newValue)); fieldPreferences.getResolvableFields().addListener((InvalidationListener) change -> @@ -1779,7 +1810,7 @@ public CleanupPreferences getCleanupPreferences() { FieldFormatterCleanups.parse(StringUtil.unifyLineBreaks(get(CLEANUP_FIELD_FORMATTERS), "")))); cleanupPreferences.getObservableActiveJobs().addListener((SetChangeListener<CleanupPreferences.CleanupStep>) c -> - putStringList(CLEANUP_JOBS, cleanupPreferences.getActiveJobs().stream().map(Enum::name).collect(Collectors.toList()))); + putStringList(CLEANUP_JOBS, cleanupPreferences.getActiveJobs().stream().map(Enum::name).toList())); EasyBind.listen(cleanupPreferences.fieldFormatterCleanupsProperty(), (fieldFormatters, oldValue, newValue) -> { putBoolean(CLEANUP_FIELD_FORMATTERS_ENABLED, newValue.isEnabled()); @@ -1986,7 +2017,7 @@ public XmpPreferences getXmpPreferences() { xmpPreferences.getXmpPrivacyFilter().addListener((SetChangeListener<Field>) c -> putStringList(XMP_PRIVACY_FILTERS, xmpPreferences.getXmpPrivacyFilter().stream() .map(Field::getName) - .collect(Collectors.toList()))); + .toList())); return xmpPreferences; } diff --git a/src/main/resources/l10n/JabRef_en.properties b/src/main/resources/l10n/JabRef_en.properties index ff6576e59d2..d0c1d2e10eb 100644 --- a/src/main/resources/l10n/JabRef_en.properties +++ b/src/main/resources/l10n/JabRef_en.properties @@ -1488,6 +1488,8 @@ Duplicated\ Journal\ File=Duplicated Journal File Error\ Occurred=Error Occurred Journal\ file\ %s\ already\ added=Journal file %s already added Name\ cannot\ be\ empty=Name cannot be empty +Toggle=Toggle +Toggle\ selected\ list\ on/off=Toggle selected list on/off Display\ keywords\ appearing\ in\ ALL\ entries=Display keywords appearing in ALL entries Display\ keywords\ appearing\ in\ ANY\ entry=Display keywords appearing in ANY entry @@ -1496,6 +1498,7 @@ None\ of\ the\ selected\ entries\ have\ citation\ keys.=None of the selected ent None\ of\ the\ selected\ entries\ have\ DOIs.=None of the selected entries have DOIs. Unabbreviate\ journal\ names=Unabbreviate journal names Unabbreviating...=Unabbreviating... +Cannot\ unabbreviate\:\ all\ journal\ lists\ are\ disabled.=Cannot unabbreviate: all journal lists are disabled. Usage=Usage diff --git a/src/test/java/org/jabref/gui/journals/AbbreviateActionTest.java b/src/test/java/org/jabref/gui/journals/AbbreviateActionTest.java new file mode 100644 index 00000000000..4c406ab3ab7 --- /dev/null +++ b/src/test/java/org/jabref/gui/journals/AbbreviateActionTest.java @@ -0,0 +1,123 @@ +package org.jabref.gui.journals; + +import java.util.Optional; + +import javax.swing.undo.UndoManager; + +import javafx.collections.FXCollections; + +import org.jabref.architecture.AllowedToUseSwing; +import org.jabref.gui.DialogService; +import org.jabref.gui.LibraryTab; +import org.jabref.gui.StateManager; +import org.jabref.gui.actions.StandardActions; +import org.jabref.logic.journals.JournalAbbreviationPreferences; +import org.jabref.logic.journals.JournalAbbreviationRepository; +import org.jabref.logic.l10n.Localization; +import org.jabref.logic.util.TaskExecutor; +import org.jabref.model.database.BibDatabaseContext; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@AllowedToUseSwing("Uses UndoManager from javax.swing") +class AbbreviateActionTest { + + @Mock + private DialogService dialogService; + + @Mock + private StateManager stateManager; + + @Mock + private LibraryTab libraryTab; + + @Mock + private JournalAbbreviationPreferences abbreviationPreferences; + + @Mock + private TaskExecutor taskExecutor; + + @Mock + private UndoManager undoManager; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + when(stateManager.getSelectedEntries()).thenReturn(FXCollections.observableArrayList()); + when(stateManager.getActiveDatabase()).thenReturn(Optional.empty()); + } + + @Test + void unabbreviateWithAllSourcesDisabledShowsNotification() { + when(abbreviationPreferences.getExternalJournalLists()).thenReturn(FXCollections.observableArrayList()); + when(abbreviationPreferences.isSourceEnabled(JournalAbbreviationRepository.BUILTIN_LIST_ID)).thenReturn(false); + + AbbreviateAction action = new AbbreviateAction( + StandardActions.UNABBREVIATE, + () -> libraryTab, + dialogService, + stateManager, + abbreviationPreferences, + taskExecutor, + undoManager); + + action.execute(); + + verify(dialogService).notify(eq(Localization.lang("Cannot unabbreviate: all journal lists are disabled."))); + } + + @Test + void unabbreviateWithOneSourceEnabledExecutesTask() { + when(abbreviationPreferences.getExternalJournalLists()).thenReturn(FXCollections.observableArrayList()); + when(abbreviationPreferences.isSourceEnabled(JournalAbbreviationRepository.BUILTIN_LIST_ID)).thenReturn(true); + + BibDatabaseContext databaseContext = mock(BibDatabaseContext.class); + when(stateManager.getActiveDatabase()).thenReturn(Optional.of(databaseContext)); + + AbbreviateAction action = new AbbreviateAction( + StandardActions.UNABBREVIATE, + () -> libraryTab, + dialogService, + stateManager, + abbreviationPreferences, + taskExecutor, + undoManager); + + action.execute(); + + verify(dialogService, never()).notify(eq(Localization.lang("Cannot unabbreviate: all journal lists are disabled."))); + verify(dialogService).notify(eq(Localization.lang("Unabbreviating..."))); + } + + @Test + void checksIfAnyJournalSourcesAreEnabled() { + when(abbreviationPreferences.getExternalJournalLists()).thenReturn( + FXCollections.observableArrayList("source1.csv", "source2.csv")); + when(abbreviationPreferences.isSourceEnabled(JournalAbbreviationRepository.BUILTIN_LIST_ID)).thenReturn(false); + when(abbreviationPreferences.isSourceEnabled("source1.csv")).thenReturn(false); + when(abbreviationPreferences.isSourceEnabled("source2.csv")).thenReturn(true); + + AbbreviateAction action = new AbbreviateAction( + StandardActions.UNABBREVIATE, + () -> libraryTab, + dialogService, + stateManager, + abbreviationPreferences, + taskExecutor, + undoManager); + + action.execute(); + + verify(dialogService, never()).notify(eq(Localization.lang("Cannot unabbreviate: all journal lists are disabled."))); + } +} + diff --git a/src/test/java/org/jabref/gui/journals/UndoableUnabbreviatorTest.java b/src/test/java/org/jabref/gui/journals/UndoableUnabbreviatorTest.java new file mode 100644 index 00000000000..f3d010e3351 --- /dev/null +++ b/src/test/java/org/jabref/gui/journals/UndoableUnabbreviatorTest.java @@ -0,0 +1,137 @@ +package org.jabref.gui.journals; + +import java.util.Set; + +import javax.swing.undo.CompoundEdit; + +import org.jabref.architecture.AllowedToUseSwing; +import org.jabref.logic.journals.Abbreviation; +import org.jabref.logic.journals.JournalAbbreviationRepository; +import org.jabref.model.database.BibDatabase; +import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.field.StandardField; +import org.jabref.model.entry.types.StandardEntryType; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@AllowedToUseSwing("UndoableUnabbreviator requires Swing Compound Edit") +class UndoableUnabbreviatorTest { + + private static final Abbreviation BUILT_IN_1 = new Abbreviation("Journal of Built-in Testing", "J. Built-in Test."); + private static final Abbreviation BUILT_IN_2 = new Abbreviation("Archives of Built-in Science", "Arch. Built-in Sci."); + + private static final Abbreviation CUSTOM_1 = new Abbreviation("Journal of Custom Testing", "J. Custom Test."); + private static final Abbreviation CUSTOM_2 = new Abbreviation("Archives of Custom Science", "Arch. Custom Sci."); + + private static final String CUSTOM_SOURCE = "custom-source"; + private JournalAbbreviationRepository repository; + private UndoableUnabbreviator unabbreviator; + private BibDatabase database; + private CompoundEdit compoundEdit; + + @BeforeEach + void setUp() { + repository = new JournalAbbreviationRepository(); + repository.addCustomAbbreviations(Set.of(BUILT_IN_1, BUILT_IN_2), + JournalAbbreviationRepository.BUILTIN_LIST_ID, + true); + + repository.addCustomAbbreviations(Set.of(CUSTOM_1, CUSTOM_2), + CUSTOM_SOURCE, + true); + + unabbreviator = new UndoableUnabbreviator(repository); + database = new BibDatabase(); + compoundEdit = new CompoundEdit(); + } + + private BibEntry createEntryWithAbbreviatedJournal(String abbreviatedJournal) { + return new BibEntry(StandardEntryType.Article) + .withField(StandardField.JOURNAL, abbreviatedJournal); + } + + @Test + void unabbreviateWithBothSourcesEnabled() { + assertTrue(repository.isSourceEnabled(JournalAbbreviationRepository.BUILTIN_LIST_ID)); + assertTrue(repository.isSourceEnabled(CUSTOM_SOURCE)); + + BibEntry builtInEntry = createEntryWithAbbreviatedJournal(BUILT_IN_1.getAbbreviation()); + boolean builtInResult = unabbreviator.unabbreviate(database, builtInEntry, StandardField.JOURNAL, compoundEdit); + + assertEquals(BUILT_IN_1.getName(), builtInEntry.getField(StandardField.JOURNAL).orElse(""), + "Journal name should be replaced with full name"); + + BibEntry customEntry = createEntryWithAbbreviatedJournal(CUSTOM_1.getAbbreviation()); + boolean customResult = unabbreviator.unabbreviate(database, customEntry, StandardField.JOURNAL, compoundEdit); + + assertEquals(CUSTOM_1.getName(), customEntry.getField(StandardField.JOURNAL).orElse(""), + "Journal name should be replaced with full name"); + } + + @Test + void unabbreviateWithOnlyBuiltInSourceEnabled() { + repository.setSourceEnabled(CUSTOM_SOURCE, false); + + assertTrue(repository.isSourceEnabled(JournalAbbreviationRepository.BUILTIN_LIST_ID)); + assertFalse(repository.isSourceEnabled(CUSTOM_SOURCE)); + + BibEntry builtInEntry = createEntryWithAbbreviatedJournal(BUILT_IN_1.getAbbreviation()); + boolean builtInResult = unabbreviator.unabbreviate(database, builtInEntry, StandardField.JOURNAL, compoundEdit); + + assertEquals(BUILT_IN_1.getName(), builtInEntry.getField(StandardField.JOURNAL).orElse(""), + "Journal name should be replaced with full name"); + + BibEntry customEntry = createEntryWithAbbreviatedJournal(CUSTOM_1.getAbbreviation()); + boolean customResult = unabbreviator.unabbreviate(database, customEntry, StandardField.JOURNAL, compoundEdit); + + assertEquals(CUSTOM_1.getAbbreviation(), customEntry.getField(StandardField.JOURNAL).orElse(""), + "Journal abbreviation should remain unchanged"); + } + + @Test + void unabbreviateWithOnlyCustomSourceEnabled() { + repository.setSourceEnabled(JournalAbbreviationRepository.BUILTIN_LIST_ID, false); + + assertFalse(repository.isSourceEnabled(JournalAbbreviationRepository.BUILTIN_LIST_ID)); + assertTrue(repository.isSourceEnabled(CUSTOM_SOURCE)); + + BibEntry builtInEntry = createEntryWithAbbreviatedJournal(BUILT_IN_1.getAbbreviation()); + boolean builtInResult = unabbreviator.unabbreviate(database, builtInEntry, StandardField.JOURNAL, compoundEdit); + + assertEquals(BUILT_IN_1.getAbbreviation(), builtInEntry.getField(StandardField.JOURNAL).orElse(""), + "Journal abbreviation should remain unchanged"); + + BibEntry customEntry = createEntryWithAbbreviatedJournal(CUSTOM_1.getAbbreviation()); + boolean customResult = unabbreviator.unabbreviate(database, customEntry, StandardField.JOURNAL, compoundEdit); + + assertEquals(CUSTOM_1.getName(), customEntry.getField(StandardField.JOURNAL).orElse(""), + "Journal name should be replaced with full name"); + } + + @Test + void unabbreviateWithBothSourcesDisabled() { + repository.setSourceEnabled(JournalAbbreviationRepository.BUILTIN_LIST_ID, false); + repository.setSourceEnabled(CUSTOM_SOURCE, false); + + assertFalse(repository.isSourceEnabled(JournalAbbreviationRepository.BUILTIN_LIST_ID)); + assertFalse(repository.isSourceEnabled(CUSTOM_SOURCE)); + + BibEntry builtInEntry = createEntryWithAbbreviatedJournal(BUILT_IN_1.getAbbreviation()); + boolean builtInResult = unabbreviator.unabbreviate(database, builtInEntry, StandardField.JOURNAL, compoundEdit); + + assertEquals(BUILT_IN_1.getAbbreviation(), builtInEntry.getField(StandardField.JOURNAL).orElse(""), + "Journal abbreviation should remain unchanged"); + + BibEntry customEntry = createEntryWithAbbreviatedJournal(CUSTOM_1.getAbbreviation()); + boolean customResult = unabbreviator.unabbreviate(database, customEntry, StandardField.JOURNAL, compoundEdit); + + assertEquals(CUSTOM_1.getAbbreviation(), customEntry.getField(StandardField.JOURNAL).orElse(""), + "Journal abbreviation should remain unchanged"); + } +} + diff --git a/src/test/java/org/jabref/gui/preferences/journals/JournalAbbreviationsViewModelTabTest.java b/src/test/java/org/jabref/gui/preferences/journals/JournalAbbreviationsViewModelTabTest.java index 52702a86957..bc9ab009495 100644 --- a/src/test/java/org/jabref/gui/preferences/journals/JournalAbbreviationsViewModelTabTest.java +++ b/src/test/java/org/jabref/gui/preferences/journals/JournalAbbreviationsViewModelTabTest.java @@ -10,6 +10,7 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +import javafx.beans.property.SimpleBooleanProperty; import javafx.collections.FXCollections; import javafx.collections.ObservableList; @@ -32,7 +33,9 @@ import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -63,6 +66,7 @@ class JournalAbbreviationsViewModelTabTest { private Path tempFolder; private final JournalAbbreviationRepository repository = JournalAbbreviationLoader.loadBuiltInRepository(); private DialogService dialogService; + private JournalAbbreviationPreferences abbreviationPreferences; static class TestAbbreviation extends Abbreviation { @@ -176,9 +180,14 @@ public static Stream<TestData> provideTestFiles() { @BeforeEach void setUpViewModel(@TempDir Path tempFolder) throws Exception { - JournalAbbreviationPreferences abbreviationPreferences = mock(JournalAbbreviationPreferences.class); + abbreviationPreferences = mock(JournalAbbreviationPreferences.class); + + when(abbreviationPreferences.isSourceEnabled(anyString())).thenReturn(true); + when(abbreviationPreferences.isSourceEnabled(JournalAbbreviationRepository.BUILTIN_LIST_ID)).thenReturn(true); + when(abbreviationPreferences.getExternalJournalLists()).thenReturn(FXCollections.observableArrayList()); dialogService = mock(DialogService.class); + this.tempFolder = tempFolder; TaskExecutor taskExecutor = new CurrentThreadTaskExecutor(); @@ -331,7 +340,7 @@ void builtInListsIncludeAllBuiltInAbbreviations() { ObservableList<Abbreviation> actualAbbreviations = FXCollections .observableArrayList(viewModel.abbreviationsProperty().stream() .map(AbbreviationViewModel::getAbbreviationObject) - .collect(Collectors.toList())); + .toList()); assertEquals(expected, actualAbbreviations); } @@ -532,4 +541,112 @@ private Path createTestFile(CsvFileNameAndContent testFile) throws IOException { Files.writeString(file, testFile.content); return file; } + + @Test + void toggleEnabledListChangesEnabledState() { + when(dialogService.showFileSaveDialog(any())).thenReturn(Optional.of(emptyTestFile)); + viewModel.addNewFile(); + viewModel.selectLastJournalFile(); + + AbbreviationsFileViewModel fileViewModel = viewModel.currentFileProperty().get(); + + assertTrue(fileViewModel.isEnabled()); + + fileViewModel.setEnabled(false); + + assertFalse(fileViewModel.isEnabled()); + + fileViewModel.setEnabled(true); + assertTrue(fileViewModel.isEnabled()); + + viewModel.markAsDirty(); + } + + @Test + void storeSettingsSavesEnabledState() throws IOException { + when(dialogService.showFileSaveDialog(any())).thenReturn(Optional.of(emptyTestFile)); + viewModel.addNewFile(); + viewModel.selectLastJournalFile(); + + AbbreviationsFileViewModel fileViewModel = viewModel.currentFileProperty().get(); + fileViewModel.setEnabled(false); + + viewModel.storeSettings(); + + fileViewModel.getAbsolutePath().ifPresent(path -> { + String fileName = path.getFileName().toString(); + verify(abbreviationPreferences).setSourceEnabled(fileName, false); + }); + } + + @Test + void addBuiltInListInitializesWithCorrectEnabledState() { + when(abbreviationPreferences.isSourceEnabled(JournalAbbreviationRepository.BUILTIN_LIST_ID)).thenReturn(false); + + JournalAbbreviationsTabViewModel testViewModel = new JournalAbbreviationsTabViewModel( + abbreviationPreferences, dialogService, new CurrentThreadTaskExecutor(), repository); + + testViewModel.addBuiltInList(); + + Optional<AbbreviationsFileViewModel> builtInViewModel = testViewModel.journalFilesProperty().stream() + .filter(vm -> vm.isBuiltInListProperty().get()) + .findFirst(); + + assertTrue(builtInViewModel.isPresent()); + assertFalse(builtInViewModel.get().isEnabled()); + } + + @Test + void enabledExternalListFiltersAbbreviationsWhenDisabled() throws IOException { + AbbreviationsFileViewModel fileViewModel = mock(AbbreviationsFileViewModel.class); + JournalAbbreviationRepository testRepository = mock(JournalAbbreviationRepository.class); + + when(fileViewModel.getAbsolutePath()).thenReturn(Optional.of(Path.of("test.csv"))); + when(fileViewModel.isBuiltInListProperty()).thenReturn(new SimpleBooleanProperty(false)); + when(fileViewModel.isEnabled()).thenReturn(true); + + JournalAbbreviationPreferences testPreferences = mock(JournalAbbreviationPreferences.class); + when(testPreferences.getExternalJournalLists()).thenReturn(FXCollections.observableArrayList("test.csv")); + + TaskExecutor taskExecutor = new CurrentThreadTaskExecutor(); + JournalAbbreviationsTabViewModel testViewModel = new JournalAbbreviationsTabViewModel( + testPreferences, dialogService, taskExecutor, testRepository); + + testViewModel.journalFilesProperty().add(fileViewModel); + + testViewModel.storeSettings(); + + verify(testPreferences).setSourceEnabled(eq("test.csv"), anyBoolean()); + } + + @Test + void disabledSourceAffectsAbbreviationFiltering() throws IOException { + CsvFileNameAndContent testFile = new CsvFileNameAndContent("unique-journal.csv", + new TestAbbreviation("Unique Journal Title", "Unique J. Title")); + Path testFilePath = createTestFile(testFile); + + JournalAbbreviationPreferences mockPrefs = mock(JournalAbbreviationPreferences.class); + when(mockPrefs.isSourceEnabled(anyString())).thenReturn(true); + when(mockPrefs.getExternalJournalLists()).thenReturn(FXCollections.observableArrayList()); + + JournalAbbreviationsTabViewModel testViewModel = new JournalAbbreviationsTabViewModel( + mockPrefs, dialogService, new CurrentThreadTaskExecutor(), repository); + + when(dialogService.showFileSaveDialog(any())).thenReturn(Optional.of(testFilePath)); + testViewModel.addNewFile(); + testViewModel.selectLastJournalFile(); + + AbbreviationsFileViewModel fileViewModel = testViewModel.currentFileProperty().get(); + + assertTrue(fileViewModel.isEnabled()); + + fileViewModel.setEnabled(false); + assertFalse(fileViewModel.isEnabled()); + + String filename = testFilePath.getFileName().toString(); + + testViewModel.storeSettings(); + + verify(mockPrefs).setSourceEnabled(eq(filename), eq(false)); + } } diff --git a/src/test/java/org/jabref/logic/journals/AbbreviationsTest.java b/src/test/java/org/jabref/logic/journals/AbbreviationsTest.java index 075148c1f53..6cac4e7cc4d 100644 --- a/src/test/java/org/jabref/logic/journals/AbbreviationsTest.java +++ b/src/test/java/org/jabref/logic/journals/AbbreviationsTest.java @@ -1,26 +1,89 @@ package org.jabref.logic.journals; +import java.util.Optional; +import java.util.Set; + import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; class AbbreviationsTest { + private static final Abbreviation DOTTED_JOURNAL = new Abbreviation("Dotted Journal", "Dotted J."); + private static final Abbreviation TEST_JOURNAL = new Abbreviation("Test Journal", "Test J."); + private JournalAbbreviationRepository repository; @BeforeEach void setUp() { - repository = JournalAbbreviationLoader.loadBuiltInRepository(); + repository = new JournalAbbreviationRepository(); + + repository.addCustomAbbreviations(Set.of(TEST_JOURNAL, DOTTED_JOURNAL), + JournalAbbreviationRepository.BUILTIN_LIST_ID, + true); } @Test void getNextAbbreviationAbbreviatesJournalTitle() { - assertEquals("2D Mater.", repository.getNextAbbreviation("2D Materials").get()); + Optional<String> abbreviation = repository.getNextAbbreviation("Test Journal"); + assertEquals("Test J.", abbreviation.orElse("WRONG")); } @Test void getNextAbbreviationConvertsAbbreviationToDotlessAbbreviation() { - assertEquals("2D Mater", repository.getNextAbbreviation("2D Mater.").get()); + Optional<String> abbreviation = repository.getNextAbbreviation("Test J."); + assertEquals("Test J", abbreviation.orElse("WRONG")); + } + + @Test + void getNextAbbreviationWrapsBackToFullName() { + Optional<String> abbreviation1 = repository.getNextAbbreviation("Test Journal"); + assertEquals("Test J.", abbreviation1.orElse("WRONG")); + + Optional<String> abbreviation2 = repository.getNextAbbreviation("Test J."); + assertEquals("Test J", abbreviation2.orElse("WRONG")); + + Optional<String> abbreviation3 = repository.getNextAbbreviation("Test J"); + assertEquals("Test Journal", abbreviation3.orElse("WRONG")); + } + + @Test + void constructorValidIsoAbbreviation() { + assertDoesNotThrow(() -> new Abbreviation("Test Entry", "Test. Ent.")); + } + + @Test + void constructorInvalidMedlineAbbreviation() { + NullPointerException exception = assertThrows(NullPointerException.class, + () -> new Abbreviation("Test Entry", null, "TE")); + } + + @Test + void getShortestUniqueAbbreviationDifferentFromIsoAbbreviation() { + Abbreviation abbreviation = new Abbreviation("Test Entry", "Test. Ent.", "TE"); + + assertEquals("TE", abbreviation.getShortestUniqueAbbreviation()); + assertNotEquals("Test. Ent.", abbreviation.getShortestUniqueAbbreviation()); + } + + @Test + void getNext() { + Abbreviation abbreviation = new Abbreviation("Test Entry", "Test. Ent."); + + assertEquals("Test. Ent.", abbreviation.getNext("Test Entry")); + assertEquals("Test Ent", abbreviation.getNext("Test. Ent.")); + assertEquals("Test Entry", abbreviation.getNext("Test Ent")); + } + + @Test + void testToString() { + Abbreviation abbreviation = new Abbreviation("Test Entry", "Test. Ent."); + + assertEquals("Abbreviation{name=Test Entry, abbreviation=Test. Ent., dotlessAbbreviation=Test Ent, shortestUniqueAbbreviation=}", + abbreviation.toString()); } } diff --git a/src/test/java/org/jabref/logic/journals/JournalAbbreviationRepositoryTest.java b/src/test/java/org/jabref/logic/journals/JournalAbbreviationRepositoryTest.java index 5407b885d1f..dc36f2e0dc9 100644 --- a/src/test/java/org/jabref/logic/journals/JournalAbbreviationRepositoryTest.java +++ b/src/test/java/org/jabref/logic/journals/JournalAbbreviationRepositoryTest.java @@ -1,11 +1,14 @@ package org.jabref.logic.journals; import java.util.List; +import java.util.Optional; import java.util.Set; import java.util.stream.Stream; import javax.swing.undo.CompoundEdit; +import javafx.collections.FXCollections; + import org.jabref.architecture.AllowedToUseSwing; import org.jabref.gui.journals.AbbreviationType; import org.jabref.gui.journals.UndoableAbbreviator; @@ -25,18 +28,49 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; @AllowedToUseSwing("UndoableUnabbreviator and UndoableAbbreviator requires Swing Compound Edit in order test the abbreviation and unabbreviation of journal titles") class JournalAbbreviationRepositoryTest { + private static final Abbreviation ACS_MATERIALS = new Abbreviation("ACS Applied Materials & Interfaces", "ACS Appl. Mater. Interfaces"); + private static final Abbreviation AMERICAN_JOURNAL = new Abbreviation("American Journal of Public Health", "Am. J. Public Health"); + private static final Abbreviation ANTIOXIDANTS = new Abbreviation("Antioxidants & Redox Signaling", "Antioxid. Redox Signaling"); + private static final Abbreviation PHYSICAL_REVIEW = new Abbreviation("Physical Review B", "Phys. Rev. B"); + private JournalAbbreviationRepository repository; - + private JournalAbbreviationPreferences abbreviationPreferences; + private final BibDatabase bibDatabase = new BibDatabase(); private UndoableUnabbreviator undoableUnabbreviator; + + /** + * Creates a test repository with pre-defined abbreviations and all sources enabled + */ + private JournalAbbreviationRepository createTestRepository() { + JournalAbbreviationRepository testRepo = new JournalAbbreviationRepository(); + + testRepo.addCustomAbbreviations(Set.of( + AMERICAN_JOURNAL, + ACS_MATERIALS, + ANTIOXIDANTS, + PHYSICAL_REVIEW + ), JournalAbbreviationRepository.BUILTIN_LIST_ID, true); + + return testRepo; + } @BeforeEach void setUp() { - repository = JournalAbbreviationLoader.loadBuiltInRepository(); + abbreviationPreferences = mock(JournalAbbreviationPreferences.class); + + when(abbreviationPreferences.isSourceEnabled(anyString())).thenReturn(true); + when(abbreviationPreferences.getExternalJournalLists()).thenReturn(FXCollections.observableArrayList()); + + repository = new JournalAbbreviationRepository(); + undoableUnabbreviator = new UndoableUnabbreviator(repository); } @@ -161,28 +195,54 @@ void duplicateKeysWithShortestUniqueAbbreviation() { @Test void getFromFullName() { - assertEquals(new Abbreviation("American Journal of Public Health", "Am. J. Public Health"), repository.get("American Journal of Public Health").get()); + repository = createTestRepository(); + + Optional<Abbreviation> result = repository.get("American Journal of Public Health"); + assertTrue(result.isPresent()); + assertEquals(AMERICAN_JOURNAL, result.get()); } @Test void getFromAbbreviatedName() { - assertEquals(new Abbreviation("American Journal of Public Health", "Am. J. Public Health"), repository.get("Am. J. Public Health").get()); + repository = createTestRepository(); + + Optional<Abbreviation> result = repository.get("Am. J. Public Health"); + assertTrue(result.isPresent()); + assertEquals(AMERICAN_JOURNAL, result.get()); } @Test void abbreviationsWithEscapedAmpersand() { - assertEquals(new Abbreviation("ACS Applied Materials & Interfaces", "ACS Appl. Mater. Interfaces"), repository.get("ACS Applied Materials & Interfaces").get()); - assertEquals(new Abbreviation("ACS Applied Materials & Interfaces", "ACS Appl. Mater. Interfaces"), repository.get("ACS Applied Materials \\& Interfaces").get()); - assertEquals(new Abbreviation("Antioxidants & Redox Signaling", "Antioxid. Redox Signaling"), repository.get("Antioxidants & Redox Signaling").get()); - assertEquals(new Abbreviation("Antioxidants & Redox Signaling", "Antioxid. Redox Signaling"), repository.get("Antioxidants \\& Redox Signaling").get()); - - repository.addCustomAbbreviation(new Abbreviation("Long & Name", "L. N.", "LN")); - assertEquals(new Abbreviation("Long & Name", "L. N.", "LN"), repository.get("Long & Name").get()); - assertEquals(new Abbreviation("Long & Name", "L. N.", "LN"), repository.get("Long \\& Name").get()); + repository = createTestRepository(); + + // Standard ampersand + Optional<Abbreviation> result = repository.get("ACS Applied Materials & Interfaces"); + assertTrue(result.isPresent()); + assertEquals(ACS_MATERIALS, result.get()); + + // Escaped ampersand + assertEquals(ACS_MATERIALS, repository.get("ACS Applied Materials \\& Interfaces").orElseThrow()); + + // Another journal with standard ampersand + assertEquals(ANTIOXIDANTS, repository.get("Antioxidants & Redox Signaling").orElseThrow()); + + // Another journal with escaped ampersand + assertEquals(ANTIOXIDANTS, repository.get("Antioxidants \\& Redox Signaling").orElseThrow()); + + // Add custom abbreviation with ampersand + Abbreviation longAndName = new Abbreviation("Long & Name", "L. N.", "LN"); + repository.addCustomAbbreviation(longAndName); + + // Test standard ampersand lookup + assertEquals(longAndName, repository.get("Long & Name").orElseThrow()); + + // Test escaped ampersand lookup + assertEquals(longAndName, repository.get("Long \\& Name").orElseThrow()); } @Test void journalAbbreviationWithEscapedAmpersand() { + repository = createTestRepository(); UndoableAbbreviator undoableAbbreviator = new UndoableAbbreviator(repository, AbbreviationType.DEFAULT, false); BibEntry entryWithEscapedAmpersandInJournal = new BibEntry(StandardEntryType.Article); @@ -196,17 +256,21 @@ void journalAbbreviationWithEscapedAmpersand() { @Test void journalUnabbreviate() { + repository = createTestRepository(); + undoableUnabbreviator = new UndoableUnabbreviator(repository); + BibEntry abbreviatedJournalEntry = new BibEntry(StandardEntryType.Article); abbreviatedJournalEntry.setField(StandardField.JOURNAL, "ACS Appl. Mater. Interfaces"); undoableUnabbreviator.unabbreviate(bibDatabase, abbreviatedJournalEntry, StandardField.JOURNAL, new CompoundEdit()); - BibEntry expectedAbbreviatedJournalEntry = new BibEntry(StandardEntryType.Article) + BibEntry expectedUnabbreviatedJournalEntry = new BibEntry(StandardEntryType.Article) .withField(StandardField.JOURNAL, "ACS Applied Materials & Interfaces"); - assertEquals(expectedAbbreviatedJournalEntry, abbreviatedJournalEntry); + assertEquals(expectedUnabbreviatedJournalEntry, abbreviatedJournalEntry); } @Test void journalAbbreviateWithoutEscapedAmpersand() { + repository = createTestRepository(); UndoableAbbreviator undoableAbbreviator = new UndoableAbbreviator(repository, AbbreviationType.DEFAULT, false); BibEntry entryWithoutEscapedAmpersandInJournal = new BibEntry(StandardEntryType.Article) @@ -220,6 +284,7 @@ void journalAbbreviateWithoutEscapedAmpersand() { @Test void journalAbbreviateWithEmptyFJournal() { + repository = createTestRepository(); UndoableAbbreviator undoableAbbreviator = new UndoableAbbreviator(repository, AbbreviationType.DEFAULT, true); BibEntry entryWithoutEscapedAmpersandInJournal = new BibEntry(StandardEntryType.Article) @@ -235,47 +300,59 @@ void journalAbbreviateWithEmptyFJournal() { @Test void unabbreviateWithJournalExistsAndFJournalNot() { + repository = createTestRepository(); + undoableUnabbreviator = new UndoableUnabbreviator(repository); + BibEntry abbreviatedJournalEntry = new BibEntry(StandardEntryType.Article) .withField(StandardField.JOURNAL, "ACS Appl. Mater. Interfaces"); undoableUnabbreviator.unabbreviate(bibDatabase, abbreviatedJournalEntry, StandardField.JOURNAL, new CompoundEdit()); - BibEntry expectedAbbreviatedJournalEntry = new BibEntry(StandardEntryType.Article) + BibEntry expectedUnabbreviatedJournalEntry = new BibEntry(StandardEntryType.Article) .withField(StandardField.JOURNAL, "ACS Applied Materials & Interfaces"); - assertEquals(expectedAbbreviatedJournalEntry, abbreviatedJournalEntry); + assertEquals(expectedUnabbreviatedJournalEntry, abbreviatedJournalEntry); } @Test void unabbreviateWithJournalExistsAndFJournalExists() { + repository = createTestRepository(); + undoableUnabbreviator = new UndoableUnabbreviator(repository); + BibEntry abbreviatedJournalEntry = new BibEntry(StandardEntryType.Article) .withField(StandardField.JOURNAL, "ACS Appl. Mater. Interfaces") .withField(AMSField.FJOURNAL, "ACS Applied Materials & Interfaces"); undoableUnabbreviator.unabbreviate(bibDatabase, abbreviatedJournalEntry, StandardField.JOURNAL, new CompoundEdit()); - BibEntry expectedAbbreviatedJournalEntry = new BibEntry(StandardEntryType.Article) + BibEntry expectedUnabbreviatedJournalEntry = new BibEntry(StandardEntryType.Article) .withField(StandardField.JOURNAL, "ACS Applied Materials & Interfaces"); - assertEquals(expectedAbbreviatedJournalEntry, abbreviatedJournalEntry); + assertEquals(expectedUnabbreviatedJournalEntry, abbreviatedJournalEntry); } @Test void journalDotlessAbbreviation() { + repository = createTestRepository(); + undoableUnabbreviator = new UndoableUnabbreviator(repository); + BibEntry abbreviatedJournalEntry = new BibEntry(StandardEntryType.Article) .withField(StandardField.JOURNAL, "ACS Appl Mater Interfaces"); undoableUnabbreviator.unabbreviate(bibDatabase, abbreviatedJournalEntry, StandardField.JOURNAL, new CompoundEdit()); - BibEntry expectedAbbreviatedJournalEntry = new BibEntry(StandardEntryType.Article) + BibEntry expectedUnabbreviatedJournalEntry = new BibEntry(StandardEntryType.Article) .withField(StandardField.JOURNAL, "ACS Applied Materials & Interfaces"); - assertEquals(expectedAbbreviatedJournalEntry, abbreviatedJournalEntry); + assertEquals(expectedUnabbreviatedJournalEntry, abbreviatedJournalEntry); } @Test void journalDotlessAbbreviationWithCurlyBraces() { + repository = createTestRepository(); + undoableUnabbreviator = new UndoableUnabbreviator(repository); + BibEntry abbreviatedJournalEntry = new BibEntry(StandardEntryType.Article) .withField(StandardField.JOURNAL, "{ACS Appl Mater Interfaces}"); undoableUnabbreviator.unabbreviate(bibDatabase, abbreviatedJournalEntry, StandardField.JOURNAL, new CompoundEdit()); - BibEntry expectedAbbreviatedJournalEntry = new BibEntry(StandardEntryType.Article) + BibEntry expectedUnabbreviatedJournalEntry = new BibEntry(StandardEntryType.Article) .withField(StandardField.JOURNAL, "ACS Applied Materials & Interfaces"); - assertEquals(expectedAbbreviatedJournalEntry, abbreviatedJournalEntry); + assertEquals(expectedUnabbreviatedJournalEntry, abbreviatedJournalEntry); } /** @@ -283,14 +360,17 @@ void journalDotlessAbbreviationWithCurlyBraces() { */ @Test void titleEmbeddedWithCurlyBracesHavingNoChangesKeepsBraces() { + repository = createTestRepository(); + undoableUnabbreviator = new UndoableUnabbreviator(repository); + BibEntry abbreviatedJournalEntry = new BibEntry(StandardEntryType.InCollection) .withField(StandardField.JOURNAL, "{The Visualization Handbook}"); undoableUnabbreviator.unabbreviate(bibDatabase, abbreviatedJournalEntry, StandardField.JOURNAL, new CompoundEdit()); - BibEntry expectedAbbreviatedJournalEntry = new BibEntry(StandardEntryType.InCollection) + BibEntry expectedUnabbreviatedJournalEntry = new BibEntry(StandardEntryType.InCollection) .withField(StandardField.JOURNAL, "{The Visualization Handbook}"); - assertEquals(expectedAbbreviatedJournalEntry, abbreviatedJournalEntry); + assertEquals(expectedUnabbreviatedJournalEntry, abbreviatedJournalEntry); } /** @@ -298,32 +378,38 @@ void titleEmbeddedWithCurlyBracesHavingNoChangesKeepsBraces() { */ @Test void titleWithNestedCurlyBracesHavingNoChangesKeepsBraces() { + repository = createTestRepository(); + undoableUnabbreviator = new UndoableUnabbreviator(repository); + BibEntry abbreviatedJournalEntry = new BibEntry(StandardEntryType.InProceedings) .withField(StandardField.BOOKTITLE, "2015 {IEEE} International Conference on Digital Signal Processing, {DSP} 2015, Singapore, July 21-24, 2015"); undoableUnabbreviator.unabbreviate(bibDatabase, abbreviatedJournalEntry, StandardField.JOURNAL, new CompoundEdit()); - BibEntry expectedAbbreviatedJournalEntry = new BibEntry(StandardEntryType.InProceedings) + BibEntry expectedUnabbreviatedJournalEntry = new BibEntry(StandardEntryType.InProceedings) .withField(StandardField.BOOKTITLE, "2015 {IEEE} International Conference on Digital Signal Processing, {DSP} 2015, Singapore, July 21-24, 2015"); - assertEquals(expectedAbbreviatedJournalEntry, abbreviatedJournalEntry); + assertEquals(expectedUnabbreviatedJournalEntry, abbreviatedJournalEntry); } @Test void dotlessForPhysRevB() { + repository = createTestRepository(); + undoableUnabbreviator = new UndoableUnabbreviator(repository); + BibEntry abbreviatedJournalEntry = new BibEntry(StandardEntryType.Article) .withField(StandardField.JOURNAL, "Phys Rev B"); undoableUnabbreviator.unabbreviate(bibDatabase, abbreviatedJournalEntry, StandardField.JOURNAL, new CompoundEdit()); - BibEntry expectedAbbreviatedJournalEntry = new BibEntry(StandardEntryType.Article) + BibEntry expectedUnabbreviatedJournalEntry = new BibEntry(StandardEntryType.Article) .withField(StandardField.JOURNAL, "Physical Review B"); - assertEquals(expectedAbbreviatedJournalEntry, abbreviatedJournalEntry); + assertEquals(expectedUnabbreviatedJournalEntry, abbreviatedJournalEntry); } @ParameterizedTest @MethodSource("provideAbbreviationTestCases") - void fuzzyMatch(List<Abbreviation> abbreviationList, String input, String expectedAbbreviation, String expectedDotless, String expectedShortest, String ambiguousInput) { - repository.addCustomAbbreviations(abbreviationList); + void fuzzyMatch(Set<Abbreviation> abbreviationSet, String input, String expectedAbbreviation, String expectedDotless, String expectedShortest, String ambiguousInput) { + repository.addCustomAbbreviations(abbreviationSet); assertEquals(expectedAbbreviation, repository.getDefaultAbbreviation(input).orElse("WRONG")); @@ -337,7 +423,7 @@ void fuzzyMatch(List<Abbreviation> abbreviationList, String input, String expect static Stream<Arguments> provideAbbreviationTestCases() { return Stream.of( Arguments.of( - List.of( + Set.of( new Abbreviation("Journal of Physics A", "J. Phys. A", "JPA"), new Abbreviation("Journal of Physics B", "J. Phys. B", "JPB"), new Abbreviation("Journal of Physics C", "J. Phys. C", "JPC") @@ -349,7 +435,7 @@ static Stream<Arguments> provideAbbreviationTestCases() { "Journal of Physics" ), Arguments.of( - List.of( + Set.of( new Abbreviation("中国物理学报", "物理学报", "ZWP"), new Abbreviation("中国物理学理", "物理学报报", "ZWP"), new Abbreviation("中国科学: 物理学", "中科物理", "ZKP") @@ -361,7 +447,7 @@ static Stream<Arguments> provideAbbreviationTestCases() { "中国物理学" ), Arguments.of( - List.of( + Set.of( new Abbreviation("Zeitschrift für Chem", "Z. Phys. Chem.", "ZPC"), new Abbreviation("Zeitschrift für Chys", "Z. Angew. Chem.", "ZAC") ), @@ -373,4 +459,209 @@ static Stream<Arguments> provideAbbreviationTestCases() { ) ); } + + @Test + void addCustomAbbreviationsWithEnabledState() { + String sourceKey = "test-source"; + + repository.addCustomAbbreviations(Set.of( + new Abbreviation("Journal One", "J. One"), + new Abbreviation("Journal Two", "J. Two") + ), sourceKey, true); + + assertEquals("J. One", repository.getDefaultAbbreviation("Journal One").orElse("WRONG")); + assertEquals("J. Two", repository.getDefaultAbbreviation("Journal Two").orElse("WRONG")); + + assertTrue(repository.isSourceEnabled(sourceKey)); + } + + @Test + void disablingSourcePreventsAccessToAbbreviations() { + String sourceKey = "test-source"; + + repository.addCustomAbbreviations(Set.of( + new Abbreviation("Unique Journal", "U. J.") + ), sourceKey, true); + + assertEquals("U. J.", repository.getDefaultAbbreviation("Unique Journal").orElse("WRONG")); + + repository.setSourceEnabled(sourceKey, false); + + assertFalse(repository.isSourceEnabled(sourceKey)); + + Optional<String> abbreviation = repository.getDefaultAbbreviation("Unique Journal"); + assertTrue(abbreviation.isEmpty()); + } + + @Test + void reenablingSourceRestoresAccessToAbbreviations() { + String sourceKey = "test-source"; + + repository.addCustomAbbreviations(Set.of( + new Abbreviation("Disabled Journal", "D. J.") + ), sourceKey, true); + + repository.setSourceEnabled(sourceKey, false); + + assertEquals("WRONG", repository.getDefaultAbbreviation("Disabled Journal").orElse("WRONG")); + + repository.setSourceEnabled(sourceKey, true); + + assertTrue(repository.isSourceEnabled(sourceKey)); + + assertEquals("D. J.", repository.getDefaultAbbreviation("Disabled Journal").orElse("WRONG")); + } + + @Test + void builtInListCanBeToggled() { + repository = createTestRepository(); + + assertTrue(repository.isSourceEnabled(JournalAbbreviationRepository.BUILTIN_LIST_ID)); + + String journalName = "American Journal of Public Health"; + String abbreviation = "Am. J. Public Health"; + + assertEquals(abbreviation, repository.getDefaultAbbreviation(journalName).orElse("WRONG")); + + repository.setSourceEnabled(JournalAbbreviationRepository.BUILTIN_LIST_ID, false); + + assertFalse(repository.isSourceEnabled(JournalAbbreviationRepository.BUILTIN_LIST_ID)); + + assertEquals("WRONG", repository.getDefaultAbbreviation(journalName).orElse("WRONG")); + + repository.setSourceEnabled(JournalAbbreviationRepository.BUILTIN_LIST_ID, true); + + assertEquals(abbreviation, repository.getDefaultAbbreviation(journalName).orElse("WRONG")); + } + + /** + * This test specifically verifies that disabling sources directly affects how abbreviations are accessed. + * We explicitly create a new test repository and check enabled state at each step. + */ + @Test + void multipleSourcesCanBeToggled() { + JournalAbbreviationRepository testRepo = new JournalAbbreviationRepository(); + + String sourceKey1 = "source-1-special"; + String sourceKey2 = "source-2-special"; + + testRepo.addCustomAbbreviations(Set.of( + new Abbreviation("Unique Journal Source One XYZ", "UniqueJS1") + ), sourceKey1, true); + + testRepo.addCustomAbbreviations(Set.of( + new Abbreviation("Unique Journal Source Two ABC", "UniqueJS2") + ), sourceKey2, true); + + assertTrue(testRepo.isSourceEnabled(sourceKey1), "Source 1 should be enabled initially"); + assertTrue(testRepo.isSourceEnabled(sourceKey2), "Source 2 should be enabled initially"); + + assertEquals("UniqueJS1", testRepo.getDefaultAbbreviation("Unique Journal Source One XYZ").orElse("WRONG")); + assertEquals("UniqueJS2", testRepo.getDefaultAbbreviation("Unique Journal Source Two ABC").orElse("WRONG")); + + // Disable first source + testRepo.setSourceEnabled(sourceKey1, false); + + assertFalse(testRepo.isSourceEnabled(sourceKey1), "Source 1 should be disabled"); + assertTrue(testRepo.isSourceEnabled(sourceKey2), "Source 2 should remain enabled"); + + assertEquals("WRONG", testRepo.getDefaultAbbreviation("Unique Journal Source One XYZ").orElse("WRONG")); + assertEquals("UniqueJS2", testRepo.getDefaultAbbreviation("Unique Journal Source Two ABC").orElse("WRONG")); + + // Disable second source + testRepo.setSourceEnabled(sourceKey2, false); + + assertFalse(testRepo.isSourceEnabled(sourceKey1), "Source 1 should remain disabled"); + assertFalse(testRepo.isSourceEnabled(sourceKey2), "Source 2 should be disabled"); + + assertEquals("WRONG", testRepo.getDefaultAbbreviation("Unique Journal Source One XYZ").orElse("WRONG")); + assertEquals("WRONG", testRepo.getDefaultAbbreviation("Unique Journal Source Two ABC").orElse("WRONG")); + } + + @Test + void noEnabledSourcesReturnsEmptyAbbreviation() { + JournalAbbreviationRepository testRepo = createTestRepository(); + + Optional<Abbreviation> result = testRepo.get("American Journal of Public Health"); + assertEquals(AMERICAN_JOURNAL, result.orElse(null)); + + testRepo.setSourceEnabled(JournalAbbreviationRepository.BUILTIN_LIST_ID, false); + assertEquals(Optional.empty(), testRepo.get("American Journal of Public Health")); + } + + @Test + void getForUnabbreviationRespectsEnabledSources() { + JournalAbbreviationRepository testRepo = createTestRepository(); + + String abbreviation = "Am. J. Public Health"; + + assertTrue(testRepo.isAbbreviatedName(abbreviation)); + + Optional<Abbreviation> result = testRepo.getForUnabbreviation(abbreviation); + assertEquals("American Journal of Public Health", result.map(Abbreviation::getName).orElse(null)); + + testRepo.setSourceEnabled(JournalAbbreviationRepository.BUILTIN_LIST_ID, false); + + Optional<Abbreviation> resultAfterDisabling = testRepo.getForUnabbreviation(abbreviation); + assertEquals(Optional.empty(), resultAfterDisabling); + } + + @Test + void isAbbreviatedNameRespectsEnabledSources() { + JournalAbbreviationRepository testRepo = createTestRepository(); + + String abbreviation = "Am. J. Public Health"; + assertTrue(testRepo.isAbbreviatedName(abbreviation), "Should recognize as abbreviation when source is enabled"); + + testRepo.setSourceEnabled(JournalAbbreviationRepository.BUILTIN_LIST_ID, false); + + assertFalse(testRepo.isAbbreviatedName(abbreviation), "Should not recognize as abbreviation when source is disabled"); + } + + @Test + void getAllAbbreviationsWithSourcesReturnsCorrectSources() { + JournalAbbreviationRepository testRepo = new JournalAbbreviationRepository(); + + testRepo.getCustomAbbreviations().clear(); + + testRepo.addCustomAbbreviations(Set.of( + AMERICAN_JOURNAL, + ACS_MATERIALS, + ANTIOXIDANTS, + PHYSICAL_REVIEW + ), JournalAbbreviationRepository.BUILTIN_LIST_ID, true); + + String customSource = "test-custom"; + testRepo.addCustomAbbreviations(Set.of( + new Abbreviation("Custom Journal", "Cust. J.") + ), customSource, true); + + List<JournalAbbreviationRepository.AbbreviationWithSource> allWithSources = testRepo.getAllAbbreviationsWithSources(); + + assertTrue(allWithSources.size() >= 5, + "Should have at least 5 abbreviations (got " + allWithSources.size() + ")"); + + long customCount = allWithSources.stream() + .filter(aws -> customSource.equals(aws.getSource())) + .count(); + assertEquals(1, customCount, "Should have 1 custom source abbreviation"); + + long builtInCount = allWithSources.stream() + .filter(aws -> JournalAbbreviationRepository.BUILTIN_LIST_ID.equals(aws.getSource())) + .count(); + assertTrue(builtInCount >= 4, "Should have at least 4 built-in abbreviations"); + + Optional<JournalAbbreviationRepository.AbbreviationWithSource> customAbbr = allWithSources.stream() + .filter(aws -> customSource.equals(aws.getSource())) + .findFirst(); + assertTrue(customAbbr.isPresent(), "Should find custom abbreviation with source"); + assertEquals("Custom Journal", customAbbr.get().getAbbreviation().getName()); + + for (Abbreviation abbr : Set.of(AMERICAN_JOURNAL, ACS_MATERIALS, ANTIOXIDANTS, PHYSICAL_REVIEW)) { + boolean found = allWithSources.stream() + .anyMatch(aws -> JournalAbbreviationRepository.BUILTIN_LIST_ID.equals(aws.getSource()) && + abbr.getName().equals(aws.getAbbreviation().getName())); + assertTrue(found, "Should find " + abbr.getName() + " with built-in source"); + } + } }