package org.jabref.gui.journals; import java.io.FileNotFoundException; import java.io.IOException; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; import java.util.Objects; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleIntegerProperty; import javafx.beans.property.SimpleListProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.collections.FXCollections; import javafx.collections.ListChangeListener; import org.jabref.gui.AbstractViewModel; import org.jabref.gui.DialogService; import org.jabref.gui.util.BackgroundTask; import org.jabref.gui.util.FileDialogConfiguration; import org.jabref.gui.util.TaskExecutor; import org.jabref.logic.journals.Abbreviation; import org.jabref.logic.journals.JournalAbbreviationLoader; import org.jabref.logic.journals.JournalAbbreviationPreferences; import org.jabref.logic.l10n.Localization; import org.jabref.logic.util.FileExtensions; import org.jabref.preferences.PreferencesService; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; /** * This class provides a model for managing journal abbreviation lists. * It provides all necessary methods to create, modify or delete journal * abbreviations and files. To visualize the model one can bind the properties * to ui elements. */ public class ManageJournalAbbreviationsViewModel extends AbstractViewModel { private final Log logger = LogFactory.getLog(ManageJournalAbbreviationsViewModel.class); private final SimpleListProperty<AbbreviationsFileViewModel> journalFiles = new SimpleListProperty<>(FXCollections.observableArrayList()); private final SimpleListProperty<AbbreviationViewModel> abbreviations = new SimpleListProperty<>(FXCollections.observableArrayList()); private final SimpleIntegerProperty abbreviationsCount = new SimpleIntegerProperty(); private final SimpleObjectProperty<AbbreviationsFileViewModel> currentFile = new SimpleObjectProperty<>(); private final SimpleObjectProperty<AbbreviationViewModel> currentAbbreviation = new SimpleObjectProperty<>(); private final SimpleBooleanProperty isFileRemovable = new SimpleBooleanProperty(); private final SimpleBooleanProperty isLoading = new SimpleBooleanProperty(false); private final SimpleBooleanProperty isLoadingBuiltIn = new SimpleBooleanProperty(false); private final SimpleBooleanProperty isLoadingIeee = new SimpleBooleanProperty(false); private final SimpleBooleanProperty isAbbreviationEditableAndRemovable = new SimpleBooleanProperty(); private final PreferencesService preferences; private final DialogService dialogService; private final TaskExecutor taskExecutor; private final JournalAbbreviationPreferences abbreviationsPreferences; private final JournalAbbreviationLoader journalAbbreviationLoader; public ManageJournalAbbreviationsViewModel(PreferencesService preferences, DialogService dialogService, TaskExecutor taskExecutor, JournalAbbreviationLoader journalAbbreviationLoader) { this.preferences = Objects.requireNonNull(preferences); this.dialogService = Objects.requireNonNull(dialogService); this.taskExecutor = Objects.requireNonNull(taskExecutor); this.journalAbbreviationLoader = Objects.requireNonNull(journalAbbreviationLoader); this.abbreviationsPreferences = preferences.getJournalAbbreviationPreferences(); abbreviationsCount.bind(abbreviations.sizeProperty()); currentAbbreviation.addListener((observable, oldvalue, newvalue) -> { boolean isAbbreviation = (newvalue != null) && !newvalue.isPseudoAbbreviation(); boolean isEditableFile = (currentFile.get() != null) && !currentFile.get().isBuiltInListProperty().get(); isAbbreviationEditableAndRemovable.set(isAbbreviation && isEditableFile); }); currentFile.addListener((observable, oldvalue, newvalue) -> { if (oldvalue != null) { abbreviations.unbindBidirectional(oldvalue.abbreviationsProperty()); currentAbbreviation.set(null); } if (newvalue != null) { isFileRemovable.set(!newvalue.isBuiltInListProperty().get()); abbreviations.bindBidirectional(newvalue.abbreviationsProperty()); if (abbreviations.size() > 0) { currentAbbreviation.set(abbreviations.get(abbreviations.size() - 1)); } } else { isFileRemovable.set(false); if (!journalFiles.isEmpty()) { currentFile.set(journalFiles.get(0)); } else { currentAbbreviation.set(null); abbreviations.clear(); } } }); journalFiles.addListener((ListChangeListener<AbbreviationsFileViewModel>) c -> { if (c.next()) { if (!c.wasReplaced()) { if (c.wasAdded() && !c.getAddedSubList().get(0).isBuiltInListProperty().get()) { currentFile.set(c.getAddedSubList().get(0)); } } } }); isLoading.bind(isLoadingBuiltIn.and(isLoadingBuiltIn)); } public SimpleBooleanProperty isLoadingProperty() { return isLoading; } public boolean isAbbreviationEditableAndRemovable() { return isAbbreviationEditableAndRemovable.get(); } /** * This will wrap the built in and ieee abbreviations in pseudo abbreviation files * and add them to the list of journal abbreviation files. */ void addBuiltInLists() { BackgroundTask .wrap(JournalAbbreviationLoader::getBuiltInAbbreviations) .onRunning(() -> isLoadingBuiltIn.setValue(true)) .onSuccess(result -> { isLoadingBuiltIn.setValue(false); addList(Localization.lang("JabRef built in list"), result); }) .onFailure(dialogService::showErrorDialogAndWait) .executeWith(taskExecutor); BackgroundTask .wrap(() -> { if (abbreviationsPreferences.useIEEEAbbreviations()) { return JournalAbbreviationLoader.getOfficialIEEEAbbreviations(); } else { return JournalAbbreviationLoader.getStandardIEEEAbbreviations(); } }) .onRunning(() -> isLoadingIeee.setValue(true)) .onSuccess(result -> { isLoadingIeee.setValue(false); addList(Localization.lang("IEEE built in list"), result); }) .onFailure(dialogService::showErrorDialogAndWait) .executeWith(taskExecutor); } private void addList(String name, List<Abbreviation> abbreviations) { List<AbbreviationViewModel> builtInListViewModel = new ArrayList<>(); abbreviations.forEach(abbreviation -> builtInListViewModel.add(new AbbreviationViewModel(abbreviation))); AbbreviationsFileViewModel fileViewModel = new AbbreviationsFileViewModel(builtInListViewModel, name); journalFiles.add(fileViewModel); } /** * Read all saved file paths and read their abbreviations */ public void createFileObjects() { List<String> externalFiles = abbreviationsPreferences.getExternalJournalLists(); externalFiles.forEach(name -> openFile(Paths.get(name))); } /** * This method shall be used to add a new journal abbreviation file to the * set of journal abbreviation files. It basically just calls the * {@link #openFile(Path)}} method */ public void addNewFile() { FileDialogConfiguration fileDialogConfiguration = new FileDialogConfiguration.Builder() .addExtensionFilter(FileExtensions.TXT) .build(); dialogService.showFileSaveDialog(fileDialogConfiguration).ifPresent(this::openFile); } /** * Checks whether the file exists or if a new file should be opened. * The file will be added to the set of journal abbreviation files. * If the file also exists its abbreviations will be read and written * to the abbreviations property. * * @param filePath to the file */ private void openFile(Path filePath) { AbbreviationsFileViewModel abbreviationsFile = new AbbreviationsFileViewModel(filePath); if (journalFiles.contains(abbreviationsFile)) { dialogService.showErrorDialogAndWait(Localization.lang("Duplicated Journal File"), Localization.lang("Journal file %s already added", filePath.toString())); return; } if (abbreviationsFile.exists()) { try { abbreviationsFile.readAbbreviations(); } catch (FileNotFoundException e) { logger.debug(e.getLocalizedMessage()); } } journalFiles.add(abbreviationsFile); } public void openFile() { FileDialogConfiguration fileDialogConfiguration = new FileDialogConfiguration.Builder() .addExtensionFilter(FileExtensions.TXT) .build(); dialogService.showFileOpenDialog(fileDialogConfiguration).ifPresent(this::openFile); } /** * This method removes the currently selected file from the set of * journal abbreviation files. This will not remove existing files from * the file system. The {@code activeFile} property will always change * to the previous file in the {@code journalFiles} list property, except * the first file is selected. If so the next file will be selected 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); } } } /** * Method to add a new abbreviation to the abbreviations list property. * It also sets the currentAbbreviation property to the new abbreviation. * * @param name of the abbreviation object * @param abbreviation of the abbreviation object */ public void addAbbreviation(String name, String abbreviation) { Abbreviation abbreviationObject = new Abbreviation(name, abbreviation); AbbreviationViewModel abbreviationViewModel = new AbbreviationViewModel(abbreviationObject); if (abbreviations.contains(abbreviationViewModel)) { dialogService.showErrorDialogAndWait(Localization.lang("Duplicated Journal Abbreviation"), Localization.lang("Abbreviation %s for journal %s already defined.", abbreviation, name)); } else { abbreviations.add(abbreviationViewModel); currentAbbreviation.set(abbreviationViewModel); } } /** * Method to change the currentAbbrevaition property to a new abbreviation. * * @param name of the abbreviation object * @param abbreviation of the abbreviation object */ public void editAbbreviation(String name, String abbreviation) { if (isAbbreviationEditableAndRemovable.get()) { Abbreviation abbreviationObject = new Abbreviation(name, abbreviation); AbbreviationViewModel abbViewModel = new AbbreviationViewModel(abbreviationObject); if (abbreviations.contains(abbViewModel)) { if (!abbViewModel.equals(currentAbbreviation.get())) { dialogService.showErrorDialogAndWait(Localization.lang("Duplicated Journal Abbreviation"), Localization.lang("Abbreviation %s for journal %s already defined.", abbreviation, name)); } else { setCurrentAbbreviationNameAndAbbreviationIfValid(name, abbreviation); } } else { setCurrentAbbreviationNameAndAbbreviationIfValid(name, abbreviation); } } } /** * Sets the name and the abbreviation of the {@code currentAbbreviation} property * to the values of the {@code abbreviationsName} and {@code abbreviationsAbbreviation} * properties. */ private void setCurrentAbbreviationNameAndAbbreviationIfValid(String name, String abbreviation) { if (name.trim().isEmpty()) { dialogService.showErrorDialogAndWait(Localization.lang("Name cannot be empty")); return; } else if (abbreviation.trim().isEmpty()) { dialogService.showErrorDialogAndWait(Localization.lang("Abbreviation cannot be empty")); return; } currentAbbreviation.get().setName(name); currentAbbreviation.get().setAbbreviation(abbreviation); } /** * Method to delete the abbreviation set in the currentAbbreviation property. * The currentAbbreviationProperty will be set to the previous or next abbreviation * in the abbreviations property if applicable. Else it will be set to {@code null} * if there are no abbreviations left. */ public void deleteAbbreviation() { if (isAbbreviationEditableAndRemovable.get()) { 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); } } } /** * Calls the {@link AbbreviationsFileViewModel#writeOrCreate()} method for each file * in the journalFiles property which will overwrite the existing files with * the content of the abbreviations property of the AbbriviationsFile. Non * existing files will be created. */ public void saveJournalAbbreviationFiles() { journalFiles.forEach(file -> { try { file.writeOrCreate(); } catch (IOException e) { logger.debug(e.getLocalizedMessage()); } }); } /** * This method stores all file paths of the files in the journalFiles property * to the global JabRef preferences. Pseudo abbreviation files will not be stored. */ private void saveExternalFilesList() { List<String> extFiles = new ArrayList<>(); journalFiles.forEach(file -> { if (!file.isBuiltInListProperty().get()) { file.getAbsolutePath().ifPresent(path -> extFiles.add(path.toAbsolutePath().toString())); } }); abbreviationsPreferences.setExternalJournalLists(extFiles); } /** * This will set the {@code currentFile} property to the {@link AbbreviationsFileViewModel} object * that was added to the {@code journalFiles} list property lastly. If there are no files in the list * property this methode will do nothing as the {@code currentFile} property is already {@code null}. */ public void selectLastJournalFile() { if (journalFiles.size() > 0) { currentFile.set(journalFilesProperty().get(journalFilesProperty().size() - 1)); } } /** * This method first saves all external files to its internal list, then writes all abbreviations * to their files and finally updates the abbreviations auto complete. It basically calls * {@link #saveExternalFilesList()}, {@link #saveJournalAbbreviationFiles() } and finally * {@link JournalAbbreviationLoader#update(JournalAbbreviationPreferences)}. */ public void saveEverythingAndUpdateAutoCompleter() { saveExternalFilesList(); saveJournalAbbreviationFiles(); // Update journal abbreviation loader journalAbbreviationLoader.update(abbreviationsPreferences); preferences.storeJournalAbbreviationPreferences(abbreviationsPreferences); } public SimpleListProperty<AbbreviationsFileViewModel> journalFilesProperty() { return this.journalFiles; } public SimpleListProperty<AbbreviationViewModel> abbreviationsProperty() { return this.abbreviations; } public SimpleIntegerProperty abbreviationsCountProperty() { return this.abbreviationsCount; } public SimpleObjectProperty<AbbreviationsFileViewModel> currentFileProperty() { return this.currentFile; } public SimpleObjectProperty<AbbreviationViewModel> currentAbbreviationProperty() { return this.currentAbbreviation; } public SimpleBooleanProperty isAbbreviationEditableAndRemovableProperty() { return this.isAbbreviationEditableAndRemovable; } public SimpleBooleanProperty isFileRemovableProperty() { return this.isFileRemovable; } public void addAbbreviation() { addAbbreviation(Localization.lang("Name"), Localization.lang("Abbreviation")); } public void init() { createFileObjects(); selectLastJournalFile(); addBuiltInLists(); } }