diff --git a/AUTHORS b/AUTHORS index 05f6f4111da..9c42e1f1bbc 100644 --- a/AUTHORS +++ b/AUTHORS @@ -210,3 +210,4 @@ Yang Zongze Yara Grassi Gouffon Yifan Peng Zhang Liang +Nikita Borovikov diff --git a/CHANGELOG.md b/CHANGELOG.md index 94035e915c3..40bb662ecb4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -70,6 +70,7 @@ We refer to [GitHub issues](https://github.com/JabRef/jabref/issues) by using `# - The Medline fetcher now normalizes the author names according to the BibTeX-Standard [#4345](https://github.com/JabRef/jabref/issues/4345) - We added an option on the Linked File Viewer to rename the attached file of an entry directly on the JabRef. [#4844](https://github.com/JabRef/jabref/issues/4844) - We added an option in the preference dialog box that allows user to enable helpful tooltips.[#3599](https://github.com/JabRef/jabref/issues/3599) +- We added a tool for extracting BibTeX entries from plain text. [#5206](https://github.com/JabRef/jabref/pull/5206) - We moved the dropdown menu for selecting the push-application from the toolbar into the external application preferences. [#674](https://github.com/JabRef/jabref/issues/674) - We removed the alphabetical ordering of the custom tabs and updated the error message when trying to create a general field with a name containing an illegal character. [#5019](https://github.com/JabRef/jabref/issues/5019) - We added a context menu to the bib(la)tex-source-editor to copy'n'paste. [#5007](https://github.com/JabRef/jabref/pull/5007) diff --git a/build.gradle b/build.gradle index 6ce97a88ba8..15c0633d027 100644 --- a/build.gradle +++ b/build.gradle @@ -94,7 +94,7 @@ dependencies { compile 'org.apache.pdfbox:fontbox:2.0.16' compile 'org.apache.pdfbox:xmpbox:2.0.16' - compile group: 'org.apache.tika', name: 'tika-parsers', version: '1.22' + compile group: 'org.apache.tika', name: 'tika-core', version: '1.22' // required for reading write-protected PDFs - see https://github.com/JabRef/jabref/pull/942#issuecomment-209252635 compile 'org.bouncycastle:bcprov-jdk15on:1.62' @@ -121,6 +121,7 @@ dependencies { compile 'com.google.guava:guava:28.0-jre' + // JavaFX stuff compile 'de.jensd:fontawesomefx-commons:8.15' compile 'de.jensd:fontawesomefx-materialdesignfont:1.7.22-4' diff --git a/docs/adr/0005-fully-support-utf8-only-for-latex-files.md b/docs/adr/0005-fully-support-utf8-only-for-latex-files.md new file mode 100644 index 00000000000..6e066e8b7d8 --- /dev/null +++ b/docs/adr/0005-fully-support-utf8-only-for-latex-files.md @@ -0,0 +1,44 @@ +# Fully Support UTF-8 Only For LaTeX Files + +## Context and Problem Statement + +The feature [search for citations](https://github.com/JabRef/help.jabref.org/issues/210) displays the content of LaTeX files. +The LaTeX files are text files and might be encoded arbitrarily. + +## Considered Options + +* Support UTF-8 encoding only +* Support ASCII encoding only +* Support (nearly) all encodings + +## Decision Outcome + +Chosen option: "Support UTF-8 encoding only", because comes out best (see below). + +### Positive Consequences + +* All content of LaTeX files are displayed in JabRef + +### Negative Consequences + +* When a LaTeX files is encoded in another encoding, the user might see strange characters in JabRef + +## Pros and Cons of the Options + +### Support UTF-8 encoding only + +* Good, because covers most tex file encodings +* Good, because easy to implement +* Bad, because does not support encodings used before around 2010 + +### Support ASCII encoding only + +* Good, because easy to implement +* Bad, because does not support any encoding at all + +### Support (nearly) all encodings + +* Good, because easy to implement +* Bad, because it relies on Apache Tika's `CharsetDetector`, which resides in `tika-parsers`. + This causes issues during compilation (see https://github.com/JabRef/jabref/pull/3421#issuecomment-524532832). + Example: `error: module java.xml.bind reads package javax.activation from both java.activation and jakarta.activation`. diff --git a/docs/adr/index.md b/docs/adr/index.md index 7bbc5b8c4fc..de1818aea3c 100644 --- a/docs/adr/index.md +++ b/docs/adr/index.md @@ -9,6 +9,7 @@ This log lists the architectural decisions for JabRef. - [ADR-0002](0002-use-slf4j-for-logging.md) - Use slf4j together with log4j2 for logging - [ADR-0003](0003-use-gradle-as-build-tool.md) - Use Gradle as build tool - [ADR-0004](0004-use-mariadb-connector.md) - Use MariaDB Connector +- [ADR-0005](0005-fully-support-utf8-only-for-latex-files.md) - Fully Support UTF-8 Only For LaTeX Files diff --git a/src/main/java/org/jabref/gui/BasePanel.java b/src/main/java/org/jabref/gui/BasePanel.java index aa0b1aa6188..189c58636ef 100644 --- a/src/main/java/org/jabref/gui/BasePanel.java +++ b/src/main/java/org/jabref/gui/BasePanel.java @@ -53,7 +53,6 @@ import org.jabref.gui.mergeentries.MergeEntriesAction; import org.jabref.gui.mergeentries.MergeWithFetchedEntryAction; import org.jabref.gui.preview.CitationStyleToClipboardWorker; -import org.jabref.gui.preview.PreviewPanel; import org.jabref.gui.specialfields.SpecialFieldDatabaseChangeListener; import org.jabref.gui.specialfields.SpecialFieldValueViewModel; import org.jabref.gui.specialfields.SpecialFieldViewModel; @@ -122,7 +121,6 @@ public class BasePanel extends StackPane { // Keeps track of the string dialog if it is open. private final Map actions = new HashMap<>(); private final SidePaneManager sidePaneManager; - private final PreviewPanel preview; private final BasePanelPreferences preferences; private final ExternalFileTypes externalFileTypes; @@ -179,8 +177,6 @@ public BasePanel(JabRefFrame frame, BasePanelPreferences preferences, BibDatabas this.getDatabase().registerListener(new UpdateTimestampListener(Globals.prefs)); this.entryEditor = new EntryEditor(this, externalFileTypes); - - this.preview = new PreviewPanel(getBibDatabaseContext(), this, dialogService, externalFileTypes, Globals.getKeyPrefs(), preferences.getPreviewPreferences()); } @Subscribe @@ -263,8 +259,6 @@ private void setupActions() { // The action for copying selected entries. actions.put(Actions.COPY, this::copy); - actions.put(Actions.PRINT_PREVIEW, new PrintPreviewAction()); - actions.put(Actions.CUT, this::cut); actions.put(Actions.DELETE, () -> delete(false)); @@ -358,19 +352,6 @@ private void setupActions() { new SpecialFieldViewModel(SpecialField.READ_STATUS, undoManager).getSpecialFieldAction(status, this.frame)); } - actions.put(Actions.TOGGLE_PREVIEW, () -> { - PreviewPreferences previewPreferences = Globals.prefs.getPreviewPreferences(); - boolean enabled = !previewPreferences.isPreviewPanelEnabled(); - PreviewPreferences newPreviewPreferences = previewPreferences.getBuilder() - .withPreviewPanelEnabled(enabled) - .build(); - Globals.prefs.storePreviewPreferences(newPreviewPreferences); - setPreviewActive(enabled); - }); - - actions.put(Actions.NEXT_PREVIEW_STYLE, this::nextPreviewStyle); - actions.put(Actions.PREVIOUS_PREVIEW_STYLE, this::previousPreviewStyle); - actions.put(Actions.SEND_AS_EMAIL, new SendAsEMailAction(frame)); actions.put(Actions.WRITE_XMP, new WriteXMPAction(this)::execute); @@ -679,7 +660,6 @@ private void createMainTable() { .stream() .findFirst() .ifPresent(entry -> { - preview.setEntry(entry); entryEditor.setEntry(entry); })); @@ -816,9 +796,7 @@ private void instantiateSearchAutoCompleter() { } private void adjustSplitter() { - if (mode == BasePanelMode.SHOWING_PREVIEW) { - splitPane.setDividerPositions(Globals.prefs.getPreviewPreferences().getPreviewPanelDividerPosition().doubleValue()); - } else if (mode == BasePanelMode.SHOWING_EDITOR) { + if (mode == BasePanelMode.SHOWING_EDITOR) { splitPane.setDividerPositions(preferences.getEntryEditorDividerPosition()); } } @@ -844,17 +822,11 @@ public void showAndEdit(BibEntry entry) { } private void showBottomPane(BasePanelMode newMode) { - Node pane; - switch (newMode) { - case SHOWING_PREVIEW: - pane = preview; - break; - case SHOWING_EDITOR: - pane = entryEditor; - break; - default: - throw new UnsupportedOperationException("new mode not recognized: " + newMode.name()); + if (newMode != BasePanelMode.SHOWING_EDITOR) { + throw new UnsupportedOperationException("new mode not recognized: " + newMode.name()); } + Node pane = entryEditor; + if (splitPane.getItems().size() == 2) { splitPane.getItems().set(1, pane); } else { @@ -870,23 +842,6 @@ private void showAndEdit() { } } - /** - * Sets the given preview panel as the bottom component in the split panel. Updates the mode to SHOWING_PREVIEW. - * - * @param entry The entry to show in the preview. - */ - private void showPreview(BibEntry entry) { - showBottomPane(BasePanelMode.SHOWING_PREVIEW); - - preview.setEntry(entry); - } - - private void showPreview() { - if (!mainTable.getSelectedEntries().isEmpty()) { - showPreview(mainTable.getSelectedEntries().get(0)); - } - } - public void nextPreviewStyle() { cyclePreview(Globals.prefs.getPreviewPreferences().getPreviewCyclePosition() + 1); } @@ -901,8 +856,7 @@ private void cyclePreview(int newPosition) { .withPreviewCyclePosition(newPosition) .build(); Globals.prefs.storePreviewPreferences(previewPreferences); - - preview.updateLayout(previewPreferences); + entryEditor.updatePreviewInTabs(previewPreferences); } /** @@ -910,7 +864,7 @@ private void cyclePreview(int newPosition) { */ public void closeBottomPane() { mode = BasePanelMode.SHOWING_NOTHING; - splitPane.getItems().removeAll(entryEditor, preview); + splitPane.getItems().remove(entryEditor); } /** @@ -932,23 +886,17 @@ public void selectNextEntry() { /** * This method is called from an EntryEditor when it should be closed. We relay to the selection listener, which * takes care of the rest. - * - * @param editor The entry editor to close. */ - public void entryEditorClosing(EntryEditor editor) { - if (Globals.prefs.getPreviewPreferences().isPreviewPanelEnabled()) { - showPreview(editor.getEntry()); - } else { - closeBottomPane(); - } + public void entryEditorClosing() { + closeBottomPane(); mainTable.requestFocus(); } /** - * Closes the entry editor or preview panel if it is showing the given entry. + * Closes the entry editor if it is showing the given entry. */ - public void ensureNotShowingBottomPanel(BibEntry entry) { - if (((mode == BasePanelMode.SHOWING_EDITOR) && (entryEditor.getEntry() == entry)) || ((mode == BasePanelMode.SHOWING_PREVIEW))) { + private void ensureNotShowingBottomPanel(BibEntry entry) { + if (((mode == BasePanelMode.SHOWING_EDITOR) && (entryEditor.getEntry() == entry))) { closeBottomPane(); } } @@ -996,7 +944,7 @@ public BibDatabase getDatabase() { return bibDatabaseContext.getDatabase(); } - public boolean showDeleteConfirmationDialog(int numberOfEntries) { + private boolean showDeleteConfirmationDialog(int numberOfEntries) { if (Globals.prefs.getBoolean(JabRefPreferences.CONFIRM_DELETE)) { String title = Localization.lang("Delete entry"); String message = Localization.lang("Really delete the selected entry?"); @@ -1029,13 +977,7 @@ private void saveDividerLocation(Number position) { return; } - if (mode == BasePanelMode.SHOWING_PREVIEW) { - PreviewPreferences previewPreferences = Globals.prefs.getPreviewPreferences() - .getBuilder() - .withPreviewPanelDividerPosition(position) - .build(); - Globals.prefs.storePreviewPreferences(previewPreferences); - } else if (mode == BasePanelMode.SHOWING_EDITOR) { + if (mode == BasePanelMode.SHOWING_EDITOR) { preferences.setEntryEditorDividerPosition(position.doubleValue()); } } @@ -1093,14 +1035,6 @@ public String formatOutputMessage(String start, int count) { return String.format("%s %d %s.", start, count, (count > 1 ? Localization.lang("entries") : Localization.lang("entry"))); } - private void setPreviewActive(boolean enabled) { - if (enabled) { - showPreview(); - } else { - preview.close(); - } - } - public CountingUndoManager getUndoManager() { return undoManager; } @@ -1353,13 +1287,4 @@ public void action() { markChangedOrUnChanged(); } } - - private class PrintPreviewAction implements BaseAction { - - @Override - public void action() { - showPreview(); - preview.print(); - } - } } diff --git a/src/main/java/org/jabref/gui/BasePanelMode.java b/src/main/java/org/jabref/gui/BasePanelMode.java index 2d6cb8daaec..9305439ad2b 100644 --- a/src/main/java/org/jabref/gui/BasePanelMode.java +++ b/src/main/java/org/jabref/gui/BasePanelMode.java @@ -6,7 +6,6 @@ public enum BasePanelMode { SHOWING_NOTHING, - SHOWING_PREVIEW, SHOWING_EDITOR, WILL_SHOW_EDITOR } diff --git a/src/main/java/org/jabref/gui/JabRefFrame.java b/src/main/java/org/jabref/gui/JabRefFrame.java index 557e26c217f..04414186ea1 100644 --- a/src/main/java/org/jabref/gui/JabRefFrame.java +++ b/src/main/java/org/jabref/gui/JabRefFrame.java @@ -52,6 +52,7 @@ import org.jabref.gui.actions.SimpleCommand; import org.jabref.gui.actions.StandardActions; import org.jabref.gui.auximport.NewSubLibraryAction; +import org.jabref.gui.bibtexextractor.ExtractBibtexAction; import org.jabref.gui.bibtexkeypattern.BibtexKeyPatternAction; import org.jabref.gui.contentselector.ManageContentSelectorAction; import org.jabref.gui.copyfiles.CopyFilesAction; @@ -772,6 +773,7 @@ private MenuBar createMenu() { factory.createMenuItem(StandardActions.FIND_UNLINKED_FILES, new FindUnlinkedFilesAction(this, stateManager)), factory.createMenuItem(StandardActions.WRITE_XMP, new OldDatabaseCommandWrapper(Actions.WRITE_XMP, this, stateManager)), factory.createMenuItem(StandardActions.COPY_LINKED_FILES, new CopyFilesAction(stateManager, this.getDialogService())), + factory.createMenuItem(StandardActions.EXTRACT_BIBTEX, new ExtractBibtexAction(stateManager)), new SeparatorMenuItem(), @@ -806,7 +808,6 @@ private MenuBar createMenu() { new SeparatorMenuItem(), - factory.createCheckMenuItem(StandardActions.TOGGLE_PREVIEW, new OldDatabaseCommandWrapper(Actions.TOGGLE_PREVIEW, this, stateManager), Globals.prefs.getPreviewPreferences().isPreviewPanelEnabled()), factory.createMenuItem(StandardActions.NEXT_PREVIEW_STYLE, new OldDatabaseCommandWrapper(Actions.NEXT_PREVIEW_STYLE, this, stateManager)), factory.createMenuItem(StandardActions.PREVIOUS_PREVIEW_STYLE, new OldDatabaseCommandWrapper(Actions.PREVIOUS_PREVIEW_STYLE, this, stateManager)), @@ -1225,6 +1226,11 @@ public EditAction(Actions command) { this.command = command; } + @Override + public String toString() { + return this.command.toString(); + } + @Override public void execute() { Node focusOwner = mainStage.getScene().getFocusOwner(); diff --git a/src/main/java/org/jabref/gui/actions/ActionFactory.java b/src/main/java/org/jabref/gui/actions/ActionFactory.java index 60625d13a1a..f0d8ec8703f 100644 --- a/src/main/java/org/jabref/gui/actions/ActionFactory.java +++ b/src/main/java/org/jabref/gui/actions/ActionFactory.java @@ -74,7 +74,7 @@ private static Label getAssociatedNode(MenuItem menuItem) { } public MenuItem configureMenuItem(Action action, Command command, MenuItem menuItem) { - ActionUtils.configureMenuItem(new JabRefAction(action, command, keyBindingRepository), menuItem); + ActionUtils.configureMenuItem(new JabRefAction(action, command, keyBindingRepository, Sources.FromMenu), menuItem); setGraphic(menuItem, action); // Show tooltips @@ -105,7 +105,7 @@ public MenuItem createMenuItem(Action action, Command command) { } public CheckMenuItem createCheckMenuItem(Action action, Command command, boolean selected) { - CheckMenuItem checkMenuItem = ActionUtils.createCheckMenuItem(new JabRefAction(action, command, keyBindingRepository)); + CheckMenuItem checkMenuItem = ActionUtils.createCheckMenuItem(new JabRefAction(action, command, keyBindingRepository, Sources.FromMenu)); checkMenuItem.setSelected(selected); setGraphic(checkMenuItem, action); @@ -127,7 +127,7 @@ public Menu createSubMenu(Action action, MenuItem... children) { } public Button createIconButton(Action action, Command command) { - Button button = ActionUtils.createButton(new JabRefAction(action, command, keyBindingRepository), ActionUtils.ActionTextBehavior.HIDE); + Button button = ActionUtils.createButton(new JabRefAction(action, command, keyBindingRepository, Sources.FromButton), ActionUtils.ActionTextBehavior.HIDE); button.getStyleClass().setAll("icon-button"); @@ -140,7 +140,7 @@ public Button createIconButton(Action action, Command command) { public ButtonBase configureIconButton(Action action, Command command, ButtonBase button) { ActionUtils.configureButton( - new JabRefAction(action, command, keyBindingRepository), + new JabRefAction(action, command, keyBindingRepository, Sources.FromButton), button, ActionUtils.ActionTextBehavior.HIDE); diff --git a/src/main/java/org/jabref/gui/actions/Actions.java b/src/main/java/org/jabref/gui/actions/Actions.java index 5348e3394ae..b113f289632 100644 --- a/src/main/java/org/jabref/gui/actions/Actions.java +++ b/src/main/java/org/jabref/gui/actions/Actions.java @@ -49,7 +49,6 @@ public enum Actions { SELECT_ALL, SEND_AS_EMAIL, TOGGLE_GROUPS, - TOGGLE_PREVIEW, UNABBREVIATE, UNDO, WRITE_XMP, diff --git a/src/main/java/org/jabref/gui/actions/JabRefAction.java b/src/main/java/org/jabref/gui/actions/JabRefAction.java index de4c8983ecd..f04deb1157c 100644 --- a/src/main/java/org/jabref/gui/actions/JabRefAction.java +++ b/src/main/java/org/jabref/gui/actions/JabRefAction.java @@ -1,5 +1,8 @@ package org.jabref.gui.actions; +import java.util.HashMap; +import java.util.Map; + import javafx.beans.binding.Bindings; import org.jabref.Globals; @@ -23,11 +26,23 @@ public JabRefAction(Action action, KeyBindingRepository keyBindingRepository) { } public JabRefAction(Action action, Command command, KeyBindingRepository keyBindingRepository) { + this(action, command, keyBindingRepository, null); + } + + /** + * especially for the track execute when the action run the same function but from different source. + * @param source is a string contains the source, for example "button" + */ + public JabRefAction(Action action, Command command, KeyBindingRepository keyBindingRepository, Sources source) { this(action, keyBindingRepository); setEventHandler(event -> { command.execute(); - trackExecute(getActionName(action, command)); + if (source == null) { + trackExecute(getActionName(action, command)); + } else { + trackUserActionSource(getActionName(action, command), source); + } }); disabledProperty().bind(command.executableProperty().not()); @@ -42,7 +57,12 @@ private String getActionName(Action action, Command command) { if (command.getClass().isAnonymousClass()) { return action.getText(); } else { - return command.getClass().getSimpleName(); + String commandName = command.getClass().getSimpleName(); + if ((command instanceof OldDatabaseCommandWrapper) || (command instanceof OldCommandWrapper) || commandName.contains("EditAction")) { + return command.toString(); + } else { + return commandName; + } } } @@ -50,4 +70,12 @@ private void trackExecute(String actionName) { Globals.getTelemetryClient() .ifPresent(telemetryClient -> telemetryClient.trackEvent(actionName)); } + + private void trackUserActionSource(String actionName, Sources source) { + Map properties = new HashMap<>(); + Map measurements = new HashMap<>(); + properties.put("Source", source.toString()); + + Globals.getTelemetryClient().ifPresent(telemetryClient -> telemetryClient.trackEvent(actionName, properties, measurements)); + } } diff --git a/src/main/java/org/jabref/gui/actions/OldCommandWrapper.java b/src/main/java/org/jabref/gui/actions/OldCommandWrapper.java index 4a37747726a..028e0b153bb 100644 --- a/src/main/java/org/jabref/gui/actions/OldCommandWrapper.java +++ b/src/main/java/org/jabref/gui/actions/OldCommandWrapper.java @@ -48,4 +48,9 @@ public ReadOnlyDoubleProperty progressProperty() { public void setExecutable(boolean executable) { this.executable.bind(BindingsHelper.constantOf(executable)); } + + @Override + public String toString() { + return this.command.toString(); + } } diff --git a/src/main/java/org/jabref/gui/actions/OldDatabaseCommandWrapper.java b/src/main/java/org/jabref/gui/actions/OldDatabaseCommandWrapper.java index 58a702e4190..bcd8301d690 100644 --- a/src/main/java/org/jabref/gui/actions/OldDatabaseCommandWrapper.java +++ b/src/main/java/org/jabref/gui/actions/OldDatabaseCommandWrapper.java @@ -51,6 +51,11 @@ public double getProgress() { return 0; } + @Override + public String toString() { + return this.command.toString(); + } + @Override public ReadOnlyDoubleProperty progressProperty() { return null; diff --git a/src/main/java/org/jabref/gui/actions/Sources.java b/src/main/java/org/jabref/gui/actions/Sources.java new file mode 100644 index 00000000000..55110553f65 --- /dev/null +++ b/src/main/java/org/jabref/gui/actions/Sources.java @@ -0,0 +1,7 @@ +package org.jabref.gui.actions; + +public enum Sources { + FromButton, + FromMenu + +} diff --git a/src/main/java/org/jabref/gui/actions/StandardActions.java b/src/main/java/org/jabref/gui/actions/StandardActions.java index 385e9a744a0..117b75ceffa 100644 --- a/src/main/java/org/jabref/gui/actions/StandardActions.java +++ b/src/main/java/org/jabref/gui/actions/StandardActions.java @@ -113,7 +113,6 @@ public enum StandardActions implements Action { EDIT_ENTRY(Localization.lang("Open entry editor"), IconTheme.JabRefIcons.EDIT_ENTRY, KeyBinding.EDIT_ENTRY), SHOW_PDF_VIEWER(Localization.lang("Open document viewer"), IconTheme.JabRefIcons.PDF_FILE), - TOGGLE_PREVIEW(Localization.lang("Entry preview"), IconTheme.JabRefIcons.TOGGLE_ENTRY_PREVIEW, KeyBinding.TOGGLE_ENTRY_PREVIEW), NEXT_PREVIEW_STYLE(Localization.lang("Next citation style"), KeyBinding.NEXT_PREVIEW_LAYOUT), PREVIOUS_PREVIEW_STYLE(Localization.lang("Previous citation style"), KeyBinding.PREVIOUS_PREVIEW_LAYOUT), SELECT_ALL(Localization.lang("Select all"), KeyBinding.SELECT_ALL), @@ -138,6 +137,7 @@ public enum StandardActions implements Action { DOWNLOAD_FULL_TEXT(Localization.lang("Search full text documents online"), IconTheme.JabRefIcons.FILE_SEARCH, KeyBinding.DOWNLOAD_FULL_TEXT), CLEANUP_ENTRIES(Localization.lang("Cleanup entries"), IconTheme.JabRefIcons.CLEANUP_ENTRIES, KeyBinding.CLEANUP), SET_FILE_LINKS(Localization.lang("Automatically set file links"), KeyBinding.AUTOMATICALLY_LINK_FILES), + EXTRACT_BIBTEX(Localization.lang("Extract BibTeX from plain text")), HELP(Localization.lang("Online help"), IconTheme.JabRefIcons.HELP, KeyBinding.HELP), HELP_KEY_PATTERNS(Localization.lang("Help on key patterns"), IconTheme.JabRefIcons.HELP, KeyBinding.HELP), diff --git a/src/main/java/org/jabref/gui/bibtexextractor/BibtexExtractor.java b/src/main/java/org/jabref/gui/bibtexextractor/BibtexExtractor.java new file mode 100644 index 00000000000..6c6fc83eddd --- /dev/null +++ b/src/main/java/org/jabref/gui/bibtexextractor/BibtexExtractor.java @@ -0,0 +1,170 @@ +package org.jabref.gui.bibtexextractor; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Calendar; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.field.StandardField; +import org.jabref.model.entry.types.EntryType; +import org.jabref.model.entry.types.StandardEntryType; + +public class BibtexExtractor { + + private static final String AUTHOR_TAG = "[author_tag]"; + private static final String URL_TAG = "[url_tag]"; + private static final String YEAR_TAG = "[year_tag]"; + private static final String PAGES_TAG = "[pages_tag]"; + + private static final String INITIALS_GROUP = "INITIALS"; + private static final String LASTNAME_GROUP = "LASTNAME"; + + private static final Pattern URL_PATTERN = Pattern.compile( + "(?:^|[\\W])((ht|f)tp(s?):\\/\\/|www\\.)" + + "(([\\w\\-]+\\.)+?([\\w\\-.~]+\\/?)*" + + "[\\p{Alnum}.,%_=?&#\\-+()\\[\\]\\*$~@!:/{};']*)", + Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL); + + private static final Pattern YEAR_PATTERN = Pattern.compile( + "\\d{4}", + Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL); + + private static final Pattern AUTHOR_PATTERN = Pattern.compile( + "(?<" + LASTNAME_GROUP + ">\\p{Lu}\\w+),?\\s(?<" + INITIALS_GROUP + ">(\\p{Lu}\\.\\s){1,2})" + + "\\s*(and|,|\\.)*", + Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL); + + private static final Pattern AUTHOR_PATTERN_2 = Pattern.compile( + "(?<" + INITIALS_GROUP + ">(\\p{Lu}\\.\\s){1,2})(?<" + LASTNAME_GROUP + ">\\p{Lu}\\w+)" + + "\\s*(and|,|\\.)*", + Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL); + + private static final Pattern PAGES_PATTERN = Pattern.compile( + "(p.)?\\s?\\d+(-\\d+)?", + Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL); + + private final List urls = new ArrayList<>(); + private final List authors = new ArrayList<>(); + private String year = ""; + private String pages = ""; + private String title = ""; + private boolean isArticle = true; + private String journalOrPublisher = ""; + + public BibEntry extract(String input) { + String inputWithoutUrls = findUrls(input); + String inputWithoutAuthors = findAuthors(inputWithoutUrls); + String inputWithoutYear = findYear(inputWithoutAuthors); + String inputWithoutPages = findPages(inputWithoutYear); + String nonParsed = findParts(inputWithoutPages); + return generateEntity(nonParsed); + } + + private BibEntry generateEntity(String input) { + EntryType type = isArticle ? StandardEntryType.Article : StandardEntryType.Book; + BibEntry extractedEntity = new BibEntry(type); + extractedEntity.setField(StandardField.AUTHOR, String.join(" and ", authors)); + extractedEntity.setField(StandardField.URL, String.join(", ", urls)); + extractedEntity.setField(StandardField.YEAR, year); + extractedEntity.setField(StandardField.PAGES, pages); + extractedEntity.setField(StandardField.TITLE, title); + if (isArticle) { + extractedEntity.setField(StandardField.JOURNAL, journalOrPublisher); + } else { + extractedEntity.setField(StandardField.PUBLISHER, journalOrPublisher); + } + extractedEntity.setField(StandardField.COMMENT, input); + return extractedEntity; + } + + private String findUrls(String input) { + Matcher matcher = URL_PATTERN.matcher(input); + while (matcher.find()) { + urls.add(input.substring(matcher.start(1), matcher.end())); + } + return fixSpaces(matcher.replaceAll(URL_TAG)); + } + + private String findYear(String input) { + Matcher matcher = YEAR_PATTERN.matcher(input); + while (matcher.find()) { + String yearCandidate = input.substring(matcher.start(), matcher.end()); + int intYearCandidate = Integer.parseInt(yearCandidate); + if ((intYearCandidate > 1700) && (intYearCandidate <= Calendar.getInstance().get(Calendar.YEAR))) { + year = yearCandidate; + return fixSpaces(input.replace(year, YEAR_TAG)); + } + } + return input; + } + + private String findAuthors(String input) { + String currentInput = findAuthorsByPattern(input, AUTHOR_PATTERN); + return findAuthorsByPattern(currentInput, AUTHOR_PATTERN_2); + } + + private String findAuthorsByPattern(String input, Pattern pattern) { + Matcher matcher = pattern.matcher(input); + while (matcher.find()) { + authors.add(GenerateAuthor(matcher.group(LASTNAME_GROUP), matcher.group(INITIALS_GROUP))); + } + return fixSpaces(matcher.replaceAll(AUTHOR_TAG)); + } + + private String GenerateAuthor(String lastName, String initials) { + return lastName + ", " + initials; + } + + private String findPages(String input) { + Matcher matcher = PAGES_PATTERN.matcher(input); + if (matcher.find()) { + pages = input.substring(matcher.start(), matcher.end()); + } + return fixSpaces(matcher.replaceFirst(PAGES_TAG)); + } + + private String fixSpaces(String input) { + return input.replaceAll("[,.!?;:]", "$0 ") + .replaceAll("\\p{Lt}", " $0") + .replaceAll("\\s+", " ").trim(); + } + + private String findParts(String input) { + ArrayList lastParts = new ArrayList<>(); + int afterAuthorsIndex = input.lastIndexOf(AUTHOR_TAG); + if (afterAuthorsIndex == -1) { + return input; + } else { + afterAuthorsIndex += AUTHOR_TAG.length(); + } + int delimiterIndex = input.lastIndexOf("//"); + if (delimiterIndex != -1) { + lastParts.add(input.substring(afterAuthorsIndex, delimiterIndex) + .replace(YEAR_TAG, "") + .replace(PAGES_TAG, "")); + lastParts.addAll(Arrays.asList(input.substring(delimiterIndex + 2).split(",|\\."))); + } else { + lastParts.addAll(Arrays.asList(input.substring(afterAuthorsIndex).split(",|\\."))); + } + int nonDigitParts = 0; + for (String part : lastParts) { + if (part.matches(".*\\d.*")) { + break; + } + nonDigitParts++; + } + if (nonDigitParts > 0) { + title = lastParts.get(0); + } + if (nonDigitParts > 1) { + journalOrPublisher = lastParts.get(1); + } + if (nonDigitParts > 2) { + isArticle = false; + } + return fixSpaces(input); + } +} diff --git a/src/main/java/org/jabref/gui/bibtexextractor/BibtexExtractorViewModel.java b/src/main/java/org/jabref/gui/bibtexextractor/BibtexExtractorViewModel.java new file mode 100644 index 00000000000..8eb694bd692 --- /dev/null +++ b/src/main/java/org/jabref/gui/bibtexextractor/BibtexExtractorViewModel.java @@ -0,0 +1,42 @@ +package org.jabref.gui.bibtexextractor; + +import java.util.HashMap; +import java.util.Map; + +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; + +import org.jabref.Globals; +import org.jabref.model.database.BibDatabaseContext; +import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.types.EntryType; +import org.jabref.model.entry.types.StandardEntryType; + +public class BibtexExtractorViewModel { + + private final StringProperty inputTextProperty = new SimpleStringProperty(""); + private final BibDatabaseContext bibdatabaseContext; + + public BibtexExtractorViewModel(BibDatabaseContext bibdatabaseContext) { + this.bibdatabaseContext = bibdatabaseContext; + } + + public StringProperty inputTextProperty() { + return this.inputTextProperty; + } + + public void startExtraction() { + + BibtexExtractor extractor = new BibtexExtractor(); + BibEntry entity = extractor.extract(inputTextProperty.getValue()); + this.bibdatabaseContext.getDatabase().insertEntry(entity); + trackNewEntry(StandardEntryType.Article); + } + + private void trackNewEntry(EntryType type) { + Map properties = new HashMap<>(); + properties.put("EntryType", type.getName()); + + Globals.getTelemetryClient().ifPresent(client -> client.trackEvent("NewEntry", properties, new HashMap<>())); + } +} diff --git a/src/main/java/org/jabref/gui/bibtexextractor/ExtractBibtexAction.java b/src/main/java/org/jabref/gui/bibtexextractor/ExtractBibtexAction.java new file mode 100644 index 00000000000..7610a6d7fa9 --- /dev/null +++ b/src/main/java/org/jabref/gui/bibtexextractor/ExtractBibtexAction.java @@ -0,0 +1,19 @@ +package org.jabref.gui.bibtexextractor; + +import org.jabref.gui.StateManager; +import org.jabref.gui.actions.SimpleCommand; + +import static org.jabref.gui.actions.ActionHelper.needsDatabase; + +public class ExtractBibtexAction extends SimpleCommand { + + public ExtractBibtexAction(StateManager stateManager) { + this.executable.bind(needsDatabase(stateManager)); + } + + @Override + public void execute() { + ExtractBibtexDialog dlg = new ExtractBibtexDialog(); + dlg.showAndWait(); + } +} diff --git a/src/main/java/org/jabref/gui/bibtexextractor/ExtractBibtexDialog.fxml b/src/main/java/org/jabref/gui/bibtexextractor/ExtractBibtexDialog.fxml new file mode 100644 index 00000000000..053024c2a6a --- /dev/null +++ b/src/main/java/org/jabref/gui/bibtexextractor/ExtractBibtexDialog.fxml @@ -0,0 +1,14 @@ + + + + + + + + +