diff --git a/CHANGELOG.md b/CHANGELOG.md index adc97d7c507..9d32677a43e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,7 @@ We refer to [GitHub issues](https://github.com/JabRef/jabref/issues) by using `# - We added a minimal height for the entry editor so that it can no longer be hidden by accident. [#4279](https://github.com/JabRef/jabref/issues/4279) - We added a new keyboard shortcut so that the entry editor could be closed by Ctrl + E. [#4222] (https://github.com/JabRef/jabref/issues/4222) - We added an option in the preference dialog box, that allows user to pick the dark or light theme option. [#4130] (https://github.com/JabRef/jabref/issues/4130) +- We updated updated the Related Articles tab to accept JSON from the new version of the Mr. DLib service diff --git a/src/main/java/org/jabref/gui/Base.css b/src/main/java/org/jabref/gui/Base.css index 6ec8d5c9482..c7c16105346 100644 --- a/src/main/java/org/jabref/gui/Base.css +++ b/src/main/java/org/jabref/gui/Base.css @@ -247,6 +247,7 @@ -fx-underline: false; -fx-border-style: null; -fx-border-color: null; + -fx-text-fill: -jr-theme; } .hyperlink:visited { diff --git a/src/main/java/org/jabref/gui/entryeditor/EntryEditor.css b/src/main/java/org/jabref/gui/entryeditor/EntryEditor.css index 47eefbdde49..313583ed222 100644 --- a/src/main/java/org/jabref/gui/entryeditor/EntryEditor.css +++ b/src/main/java/org/jabref/gui/entryeditor/EntryEditor.css @@ -79,3 +79,13 @@ .code-area .text { -fx-fill: -fx-text-background-color; } + +.gdpr-dialog { + -fx-font-size: 14pt; +} + +.related-articles-tab { + -fx-padding: 20 20 20 20; +} + + diff --git a/src/main/java/org/jabref/gui/entryeditor/EntryEditor.java b/src/main/java/org/jabref/gui/entryeditor/EntryEditor.java index 75070434db0..46ae9d3d32d 100644 --- a/src/main/java/org/jabref/gui/entryeditor/EntryEditor.java +++ b/src/main/java/org/jabref/gui/entryeditor/EntryEditor.java @@ -253,7 +253,7 @@ private List createTabs() { // Special tabs tabs.add(new MathSciNetTab()); tabs.add(new FileAnnotationTab(panel.getAnnotationCache())); - tabs.add(new RelatedArticlesTab(preferences)); + tabs.add(new RelatedArticlesTab(preferences, dialogService)); // Source tab sourceTab = new SourceTab(databaseContext, undoManager, preferences.getLatexFieldFormatterPreferences(), preferences.getImportFormatPreferences(), fileMonitor); diff --git a/src/main/java/org/jabref/gui/entryeditor/EntryEditorPreferences.java b/src/main/java/org/jabref/gui/entryeditor/EntryEditorPreferences.java index fd8e1d097b2..ed059ab9f7f 100644 --- a/src/main/java/org/jabref/gui/entryeditor/EntryEditorPreferences.java +++ b/src/main/java/org/jabref/gui/entryeditor/EntryEditorPreferences.java @@ -16,16 +16,18 @@ public class EntryEditorPreferences { private final BibtexKeyPatternPreferences bibtexKeyPatternPreferences; private final List customTabFieldNames; private final boolean shouldShowRecommendationsTab; + private final boolean isMrdlibAccepted; private boolean showSourceTabByDefault; private final KeyBindingRepository keyBindings; private boolean avoidOverwritingCiteKey; - public EntryEditorPreferences(Map> entryEditorTabList, LatexFieldFormatterPreferences latexFieldFormatterPreferences, ImportFormatPreferences importFormatPreferences, List customTabFieldNames, boolean shouldShowRecommendationsTab, boolean showSourceTabByDefault, BibtexKeyPatternPreferences bibtexKeyPatternPreferences, KeyBindingRepository keyBindings, boolean avoidOverwritingCiteKey) { + public EntryEditorPreferences(Map> entryEditorTabList, LatexFieldFormatterPreferences latexFieldFormatterPreferences, ImportFormatPreferences importFormatPreferences, List customTabFieldNames, boolean shouldShowRecommendationsTab, boolean isMrdlibAccepted, boolean showSourceTabByDefault, BibtexKeyPatternPreferences bibtexKeyPatternPreferences, KeyBindingRepository keyBindings, boolean avoidOverwritingCiteKey) { this.entryEditorTabList = entryEditorTabList; this.latexFieldFormatterPreferences = latexFieldFormatterPreferences; this.importFormatPreferences = importFormatPreferences; this.customTabFieldNames = customTabFieldNames; this.shouldShowRecommendationsTab = shouldShowRecommendationsTab; + this.isMrdlibAccepted = isMrdlibAccepted; this.showSourceTabByDefault = showSourceTabByDefault; this.bibtexKeyPatternPreferences = bibtexKeyPatternPreferences; this.keyBindings = keyBindings; @@ -52,6 +54,10 @@ public boolean shouldShowRecommendationsTab() { return shouldShowRecommendationsTab; } + public boolean isMrdlibAccepted() { + return isMrdlibAccepted; + } + public boolean showSourceTabByDefault() { return showSourceTabByDefault; } diff --git a/src/main/java/org/jabref/gui/entryeditor/RelatedArticlesTab.java b/src/main/java/org/jabref/gui/entryeditor/RelatedArticlesTab.java index 233d3207ed2..dbbe9ff096a 100644 --- a/src/main/java/org/jabref/gui/entryeditor/RelatedArticlesTab.java +++ b/src/main/java/org/jabref/gui/entryeditor/RelatedArticlesTab.java @@ -1,82 +1,155 @@ package org.jabref.gui.entryeditor; -import java.net.URL; +import java.io.IOException; import java.util.List; -import java.util.Optional; +import javafx.scene.control.Button; +import javafx.scene.control.Hyperlink; import javafx.scene.control.ProgressIndicator; +import javafx.scene.control.ScrollPane; import javafx.scene.control.Tooltip; +import javafx.scene.layout.HBox; import javafx.scene.layout.StackPane; -import javafx.scene.web.WebView; +import javafx.scene.layout.VBox; +import javafx.scene.text.Font; +import javafx.scene.text.FontPosture; +import javafx.scene.text.Text; import org.jabref.Globals; -import org.jabref.gui.icon.IconTheme; +import org.jabref.gui.DialogService; +import org.jabref.gui.desktop.JabRefDesktop; import org.jabref.gui.util.BackgroundTask; -import org.jabref.gui.util.OpenHyperlinksInExternalBrowser; import org.jabref.logic.importer.fetcher.MrDLibFetcher; import org.jabref.logic.l10n.Localization; import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.FieldName; import org.jabref.preferences.JabRefPreferences; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * GUI for tab displaying article recommendations based on the currently selected BibEntry + */ public class RelatedArticlesTab extends EntryEditorTab { + private static final Logger LOGGER = LoggerFactory.getLogger(RelatedArticlesTab.class); private final EntryEditorPreferences preferences; + private final DialogService dialogService; - public RelatedArticlesTab(EntryEditorPreferences preferences) { + public RelatedArticlesTab(EntryEditorPreferences preferences, DialogService dialogService) { setText(Localization.lang("Related articles")); setTooltip(new Tooltip(Localization.lang("Related articles"))); this.preferences = preferences; + this.dialogService = dialogService; } - private StackPane getPane(BibEntry entry) { + /** + * Gets a StackPane of related article information to be displayed in the Related Articles tab + * @param entry The currently selected BibEntry on the JabRef UI. + * @return A StackPane with related article information to be displayed in the Related Articles tab. + */ + private StackPane getRelatedArticlesPane(BibEntry entry) { StackPane root = new StackPane(); + root.getStyleClass().add("related-articles-tab"); ProgressIndicator progress = new ProgressIndicator(); progress.setMaxSize(100, 100); - WebView browser = new WebView(); - root.getChildren().addAll(browser, progress); MrDLibFetcher fetcher = new MrDLibFetcher(Globals.prefs.get(JabRefPreferences.LANGUAGE), - Globals.BUILD_INFO.getVersion().getFullVersion()); + Globals.BUILD_INFO.getVersion()); BackgroundTask - .wrap(() -> fetcher.performSearch(entry)) - .onRunning(() -> progress.setVisible(true)) - .onSuccess(relatedArticles -> { - progress.setVisible(false); - browser.getEngine().loadContent(convertToHtml(relatedArticles)); - }) - .executeWith(Globals.TASK_EXECUTOR); + .wrap(() -> fetcher.performSearch(entry)) + .onRunning(() -> progress.setVisible(true)) + .onSuccess(relatedArticles -> { + progress.setVisible(false); + root.getChildren().add(getRelatedArticleInfo(relatedArticles)); + }) + .executeWith(Globals.TASK_EXECUTOR); - browser.getEngine().getLoadWorker().stateProperty().addListener(new OpenHyperlinksInExternalBrowser(browser)); + root.getChildren().add(progress); return root; } /** - * Takes a List of HTML snippets stored in the field "html_representation" of a list of bibentries - * - * @param list of bib entries having a field html_representation + * Creates a VBox of the related article information to be used in the StackPane displayed in the Related Articles tab + * @param list List of BibEntries of related articles + * @return VBox of related article descriptions to be displayed in the Related Articles tab */ - private String convertToHtml(List list) { - StringBuilder htmlContent = new StringBuilder(); - URL url = IconTheme.getIconUrl("mdlListIcon"); - htmlContent - .append(""); - htmlContent.append("
    "); - list.stream() - .map(bibEntry -> bibEntry.getField("html_representation")) - .filter(Optional::isPresent) - .map(o -> "
  • " + o.get() + "
  • ") - .forEach(html -> htmlContent.append(html)); - htmlContent.append("
"); - htmlContent.append("
"); - htmlContent.append(""); - return htmlContent.toString(); + private VBox getRelatedArticleInfo(List list) { + VBox vBox = new VBox(); + vBox.setSpacing(20.0); + + for (BibEntry entry : list) { + HBox hBox = new HBox(); + hBox.setSpacing(5.0); + + String title = entry.getTitle().orElse(""); + String journal = entry.getField(FieldName.JOURNAL).orElse(""); + String authors = entry.getField(FieldName.AUTHOR).orElse(""); + String year = entry.getField(FieldName.YEAR).orElse(""); + + Hyperlink titleLink = new Hyperlink(title); + Text journalText = new Text(journal); + journalText.setFont(Font.font(Font.getDefault().getFamily(), FontPosture.ITALIC, Font.getDefault().getSize())); + Text authorsText = new Text(authors); + Text yearText = new Text("(" + year + ")"); + titleLink.setOnAction(event -> { + if (entry.getField(FieldName.URL).isPresent()) { + try { + JabRefDesktop.openBrowser(entry.getField(FieldName.URL).get()); + } catch (IOException e) { + LOGGER.error("Error opening the browser to: " + entry.getField(FieldName.URL).get(), e); + dialogService.showErrorDialogAndWait(e); + } + } + }); + + hBox.getChildren().addAll(titleLink, journalText, authorsText, yearText); + vBox.getChildren().add(hBox); + } + return vBox; + } + + /** + * Returns a consent dialog used to ask permission to send data to Mr. DLib. + * @param entry Currently selected BibEntry. (required to allow reloading of pane if accepted) + * @return StackPane returned to be placed into Related Articles tab. + */ + private ScrollPane getPrivacyDialog(BibEntry entry) { + ScrollPane root = new ScrollPane(); + root.getStyleClass().add("related-articles-tab"); + VBox vbox = new VBox(); + vbox.getStyleClass().add("gdpr-dialog"); + vbox.setSpacing(20.0); + + Button button = new Button(Localization.lang("I Agree")); + button.setDefaultButton(true); + Text line1 = new Text(Localization.lang("Mr. DLib is an external service which provides article recommendations based on the currently selected entry. Data about the selected entry must be sent to Mr. DLib in order to provide these recommendations. Do you agree that this data may be sent?")); + + line1.setWrappingWidth(1300.0); + Text line2 = new Text(Localization.lang("This setting may be changed in preferences at any time.")); + Hyperlink mdlLink = new Hyperlink(Localization.lang("Further information about Mr DLib. for JabRef users.")); + mdlLink.setOnAction(event -> { + try { + JabRefDesktop.openBrowser("http://mr-dlib.org/information-for-users/information-about-mr-dlib-for-jabref-users/"); + } catch (IOException e) { + LOGGER.error("Error opening the browser to Mr. DLib information page.", e); + dialogService.showErrorDialogAndWait(e); + } + }); + + button.setOnAction(event -> { + JabRefPreferences prefs = JabRefPreferences.getInstance(); + prefs.putBoolean(JabRefPreferences.ACCEPT_RECOMMENDATIONS, true); + dialogService.showWarningDialogAndWait(Localization.lang("Restart"), Localization.lang("Please restart JabRef for preferences to take effect.")); + setContent(getRelatedArticlesPane(entry)); + }); + + vbox.getChildren().addAll(line1, mdlLink, line2, button); + root.setContent(vbox); + + return root; } @Override @@ -86,6 +159,11 @@ public boolean shouldShow(BibEntry entry) { @Override protected void bindToEntry(BibEntry entry) { - setContent(getPane(entry)); + // Ask for consent to send data to Mr. DLib on first time to tab + if (preferences.isMrdlibAccepted()) { + setContent(getRelatedArticlesPane(entry)); + } else { + setContent(getPrivacyDialog(entry)); + } } } diff --git a/src/main/java/org/jabref/gui/preferences/EntryEditorPrefsTab.java b/src/main/java/org/jabref/gui/preferences/EntryEditorPrefsTab.java index 4530cb88157..ab874bf1287 100644 --- a/src/main/java/org/jabref/gui/preferences/EntryEditorPrefsTab.java +++ b/src/main/java/org/jabref/gui/preferences/EntryEditorPrefsTab.java @@ -27,6 +27,7 @@ class EntryEditorPrefsTab extends Pane implements PrefsTab { private final CheckBox emacsRebindCtrlF; private final CheckBox autoComplete; private final CheckBox recommendations; + private final CheckBox acceptRecommendations; private final CheckBox validation; private final RadioButton autoCompBoth; private final RadioButton autoCompFF; @@ -51,6 +52,7 @@ public EntryEditorPrefsTab(JabRefPreferences prefs) { emacsRebindCtrlF = new CheckBox(Localization.lang("Rebind C-f, too")); autoComplete = new CheckBox(Localization.lang("Enable word/name autocompletion")); recommendations = new CheckBox(Localization.lang("Show 'Related Articles' tab")); + acceptRecommendations = new CheckBox(Localization.lang("Accept recommendations from Mr. DLib")); validation = new CheckBox(Localization.lang("Show validation messages")); // allowed name formats @@ -84,8 +86,9 @@ public EntryEditorPrefsTab(JabRefPreferences prefs) { builder.add(emacsRebindCtrlA, 1, 5); builder.add(emacsRebindCtrlF, 1, 6); builder.add(recommendations, 1, 7); - builder.add(validation, 1, 8); - builder.add(new Label(""), 1, 9); + builder.add(acceptRecommendations, 1, 8); + builder.add(validation, 1, 9); + builder.add(new Label(""), 1, 10); Label autocompletionOptions = new Label(Localization.lang("Autocompletion options")); autocompletionOptions.getStyleClass().add("sectionHeader"); @@ -113,6 +116,7 @@ public EntryEditorPrefsTab(JabRefPreferences prefs) { builder.add(firstNameModeBoth, 1, 22); } + @Override public Node getBuilder() { return builder; } @@ -135,6 +139,7 @@ public void setValues() { emacsRebindCtrlA.setSelected(prefs.getBoolean(JabRefPreferences.EDITOR_EMACS_KEYBINDINGS_REBIND_CA)); emacsRebindCtrlF.setSelected(prefs.getBoolean(JabRefPreferences.EDITOR_EMACS_KEYBINDINGS_REBIND_CF)); recommendations.setSelected(prefs.getBoolean(JabRefPreferences.SHOW_RECOMMENDATIONS)); + acceptRecommendations.setSelected(prefs.getBoolean(JabRefPreferences.ACCEPT_RECOMMENDATIONS)); autoComplete.setSelected(autoCompletePreferences.shouldAutoComplete()); autoCompFields.setText(autoCompletePreferences.getCompleteNamesAsString()); @@ -171,6 +176,7 @@ public void storeSettings() { prefs.putBoolean(JabRefPreferences.AUTO_OPEN_FORM, autoOpenForm.isSelected()); prefs.putBoolean(JabRefPreferences.DEFAULT_SHOW_SOURCE, defSource.isSelected()); prefs.putBoolean(JabRefPreferences.SHOW_RECOMMENDATIONS, recommendations.isSelected()); + prefs.putBoolean(JabRefPreferences.ACCEPT_RECOMMENDATIONS, acceptRecommendations.isSelected()); prefs.putBoolean(JabRefPreferences.VALIDATE_IN_ENTRY_EDITOR, validation.isSelected()); boolean emacsModeChanged = prefs.getBoolean(JabRefPreferences.EDITOR_EMACS_KEYBINDINGS) != emacsMode.isSelected(); boolean emacsRebindCtrlAChanged = prefs.getBoolean(JabRefPreferences.EDITOR_EMACS_KEYBINDINGS_REBIND_CA) != emacsRebindCtrlA.isSelected(); diff --git a/src/main/java/org/jabref/logic/importer/fetcher/MrDLibFetcher.java b/src/main/java/org/jabref/logic/importer/fetcher/MrDLibFetcher.java index 199d68dee32..2ae7db36e35 100644 --- a/src/main/java/org/jabref/logic/importer/fetcher/MrDLibFetcher.java +++ b/src/main/java/org/jabref/logic/importer/fetcher/MrDLibFetcher.java @@ -1,6 +1,3 @@ -/** - * - */ package org.jabref.logic.importer.fetcher; import java.io.IOException; @@ -14,8 +11,8 @@ import org.jabref.logic.importer.FetcherException; import org.jabref.logic.importer.ParserResult; import org.jabref.logic.importer.fileformat.MrDLibImporter; -import org.jabref.logic.l10n.Localization; import org.jabref.logic.net.URLDownload; +import org.jabref.logic.util.Version; import org.jabref.model.database.BibDatabase; import org.jabref.model.entry.BibEntry; import org.jabref.model.entry.FieldName; @@ -25,17 +22,16 @@ import org.slf4j.LoggerFactory; /** - * This class is responible to get the recommendations from MDL + * This class is responsible for getting the recommendations from Mr. DLib */ public class MrDLibFetcher implements EntryBasedFetcher { private static final Logger LOGGER = LoggerFactory.getLogger(MrDLibFetcher.class); - private static final String NAME = "MDL_FETCHER"; private final String LANGUAGE; - private final String VERSION; + private final Version VERSION; - public MrDLibFetcher(String language, String version) { + public MrDLibFetcher(String language, Version version) { LANGUAGE = language; VERSION = version; } @@ -51,22 +47,22 @@ public List performSearch(BibEntry entry) throws FetcherException { if (title.isPresent()) { String response = makeServerRequest(title.get()); MrDLibImporter importer = new MrDLibImporter(); - ParserResult parserResult = new ParserResult(); + ParserResult parserResult; try { if (importer.isRecognizedFormat(response)) { parserResult = importer.importDatabase(response); } else { // For displaying An ErrorMessage + String error = importer.getResponseErrorMessage(response); BibEntry errorBibEntry = new BibEntry(); - errorBibEntry.setField("html_representation", - Localization.lang("Error while fetching from %0", "Mr.DLib")); + errorBibEntry.setField("html_representation", error); BibDatabase errorBibDataBase = new BibDatabase(); errorBibDataBase.insertEntry(errorBibEntry); parserResult = new ParserResult(errorBibDataBase); } } catch (IOException e) { LOGGER.error(e.getMessage(), e); - throw new FetcherException("XML Parser IOException."); + throw new FetcherException("JSON Parser IOException."); } return parserResult.getDatabase().getEntries(); } else { @@ -78,13 +74,13 @@ public List performSearch(BibEntry entry) throws FetcherException { /** * Contact the server with the title of the selected item * - * @param query: The query holds the title of the selected entry. Used to make a query to the MDL Server + * @param queryByTitle: The query holds the title of the selected entry. Used to make a query to the MDL Server * @return Returns the server response. This is an XML document as a String. */ private String makeServerRequest(String queryByTitle) throws FetcherException { try { URLDownload urlDownload = new URLDownload(constructQuery(queryByTitle)); - urlDownload.bypassSSLVerification(); + URLDownload.bypassSSLVerification(); String response = urlDownload.asString(); //Conversion of < and > @@ -97,29 +93,33 @@ private String makeServerRequest(String queryByTitle) throws FetcherException { } /** - * Constructs the query based on title of the bibentry. Adds statistical stuff to the url. + * Constructs the query based on title of the BibEntry. Adds statistical stuff to the url. * - * @param query: the title of the bib entry. + * @param queryWithTitle: the title of the bib entry. * @return the string used to make the query at mdl server */ private String constructQuery(String queryWithTitle) { // The encoding does not work for / so we convert them by our own - queryWithTitle = queryWithTitle.replaceAll("/", "convbckslsh"); + queryWithTitle = queryWithTitle.replaceAll("/", " "); URIBuilder builder = new URIBuilder(); - builder.setScheme("https"); - builder.setHost("api.mr-dlib.org"); - builder.setPath("/v1/documents/" + queryWithTitle + "/related_documents"); + builder.setScheme("http"); + builder.setHost(getMdlUrl()); + builder.setPath("/v2/items/" + queryWithTitle + "/related_items"); builder.addParameter("partner_id", "jabref"); builder.addParameter("app_id", "jabref_desktop"); - builder.addParameter("app_version", VERSION); + builder.addParameter("app_version", VERSION.getFullVersion()); builder.addParameter("app_lang", LANGUAGE); - URI uri = null; try { - uri = builder.build(); + URI uri = builder.build(); + LOGGER.trace("Request: " + uri.toString()); return uri.toString(); } catch (URISyntaxException e) { LOGGER.error(e.getMessage(), e); } return ""; } + + private String getMdlUrl() { + return VERSION.isDevelopmentVersion() ? "api-dev.darwingoliath.com" : "api.darwingoliath.com"; + } } diff --git a/src/main/java/org/jabref/logic/importer/fileformat/MrDLibImporter.java b/src/main/java/org/jabref/logic/importer/fileformat/MrDLibImporter.java index 491eff2c8d1..6320b8d7483 100644 --- a/src/main/java/org/jabref/logic/importer/fileformat/MrDLibImporter.java +++ b/src/main/java/org/jabref/logic/importer/fileformat/MrDLibImporter.java @@ -4,58 +4,45 @@ package org.jabref.logic.importer.fileformat; import java.io.BufferedReader; -import java.io.ByteArrayInputStream; import java.io.IOException; -import java.io.InputStream; import java.util.ArrayList; +import java.util.Iterator; import java.util.List; -import java.util.Locale; import java.util.stream.Collectors; -import javax.xml.parsers.ParserConfigurationException; -import javax.xml.parsers.SAXParser; -import javax.xml.parsers.SAXParserFactory; - import org.jabref.logic.importer.Importer; import org.jabref.logic.importer.ParserResult; +import org.jabref.logic.l10n.Localization; import org.jabref.logic.util.StandardFileType; import org.jabref.model.database.BibDatabase; import org.jabref.model.entry.BibEntry; import org.jabref.model.entry.FieldName; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.xml.sax.Attributes; -import org.xml.sax.SAXException; -import org.xml.sax.helpers.DefaultHandler; /** - * - * + * Handles importing of recommended articles to be displayed in the Related Articles tab. */ public class MrDLibImporter extends Importer { + private static final String DEFAULT_MRDLIB_ERROR_MESSAGE = Localization.lang("Error while fetching from Mr.DLib."); private static final Logger LOGGER = LoggerFactory.getLogger(MrDLibImporter.class); public ParserResult parserResult; + @SuppressWarnings("unused") @Override public boolean isRecognizedFormat(BufferedReader input) throws IOException { String recommendationsAsString = convertToString(input); - // check for valid format try { - SAXParserFactory factory = SAXParserFactory.newInstance(); - SAXParser saxParser = factory.newSAXParser(); - DefaultHandler handler = new DefaultHandler() { - // No Processing here. Just check for valid xml. - // Later here will be the check against the XML schema. - }; - - try (InputStream stream = new ByteArrayInputStream(recommendationsAsString.getBytes())) { - saxParser.parse(stream, handler); - } catch (Exception e) { + JSONObject jsonObject = new JSONObject(recommendationsAsString); + if (!jsonObject.has("recommendations")) { return false; } - } catch (ParserConfigurationException | SAXException e) { + } catch (JSONException ex) { return false; } return true; @@ -74,18 +61,18 @@ public String getName() { @Override public StandardFileType getFileType() { - return StandardFileType.XML; + return StandardFileType.JSON; } @Override public String getDescription() { - return "Takes valid xml documents. Parses from MrDLib API a BibEntry"; + return "Takes valid JSON documents from the Mr. DLib API and parses them into a BibEntry"; } /** - * The SaxParser needs this String. So I convert it here. - * @param Takes a BufferedReader with a reference to the XML document delivered by mdl server. - * @return Returns an String containing the XML file. + * Convert Buffered Reader response to string for JSON parsing. + * @param input Takes a BufferedReader with a reference to the JSON document delivered by mdl server. + * @return Returns an String containing the JSON document. * @throws IOException */ private String convertToString(BufferedReader input) throws IOException { @@ -117,7 +104,7 @@ public RankedBibEntry(BibEntry entry, Integer rank) { /** * Parses the input from the server to a ParserResult - * @param input A BufferedReader with a reference to a string with the servers response + * @param input A BufferedReader with a reference to a string with the server's response * @throws IOException */ private void parse(BufferedReader input) throws IOException { @@ -126,133 +113,95 @@ private void parse(BufferedReader input) throws IOException { // The document to parse String recommendations = convertToString(input); // The sorted BibEntries gets stored here later - List bibEntries = new ArrayList<>(); - //Parsing the response with a SAX parser - try { - SAXParserFactory factory = SAXParserFactory.newInstance(); - SAXParser saxParser = factory.newSAXParser(); - MrDlibImporterHandler handler = new MrDlibImporterHandler(); - try (InputStream stream = new ByteArrayInputStream(recommendations.getBytes())) { - saxParser.parse(stream, handler); - } catch (SAXException e) { - LOGGER.error(e.getMessage(), e); - } - List rankedBibEntries = handler.getRankedBibEntries(); - rankedBibEntries.sort((RankedBibEntry rankedBibEntry1, - RankedBibEntry rankedBibEntry2) -> rankedBibEntry1.rank.compareTo(rankedBibEntry2.rank)); - bibEntries = rankedBibEntries.stream().map(e -> e.entry).collect(Collectors.toList()); - } catch (ParserConfigurationException | SAXException e) { - LOGGER.error(e.getMessage(), e); + List rankedBibEntries = new ArrayList<>(); + + // Get recommendations from response and populate bib entries + JSONObject recommendationsJson = new JSONObject(recommendations).getJSONObject("recommendations"); + Iterator keys = recommendationsJson.keys(); + while (keys.hasNext()) { + String key = keys.next(); + JSONObject value = recommendationsJson.getJSONObject(key); + rankedBibEntries.add(populateBibEntry(value)); } + // Sort bib entries according to rank + rankedBibEntries.sort((RankedBibEntry rankedBibEntry1, + RankedBibEntry rankedBibEntry2) -> rankedBibEntry1.rank.compareTo(rankedBibEntry2.rank)); + List bibEntries = rankedBibEntries.stream().map(e -> e.entry).collect(Collectors.toList()); + for (BibEntry bibentry : bibEntries) { bibDatabase.insertEntry(bibentry); } - parserResult = new ParserResult(bibDatabase); } - public ParserResult getParserResult() { - return parserResult; - } - /** - * Handler that parses the response from Mr. DLib to BibEntries + * Parses the JSON recommendations into bib entries + * @param recommendation JSON object of a single recommendation returned by Mr. DLib + * @return A ranked bib entry created from the recommendation input */ - private class MrDlibImporterHandler extends DefaultHandler { - - // The list ob BibEntries with its associated rank - private final List rankedBibEntries = new ArrayList<>(); - - private boolean authors; - private boolean published_in; - private boolean title; - private boolean year; - private boolean snippet; - private boolean rank; - private boolean type; - private String htmlSnippetSingle; - private int htmlSnippetSingleRank = -1; - private BibEntry currentEntry; - - public List getRankedBibEntries() { - return rankedBibEntries; - } + private RankedBibEntry populateBibEntry(JSONObject recommendation) { + BibEntry current = new BibEntry(); + + // parse each of the relevant fields into variables + String authors = isRecommendationFieldPresent(recommendation, "authors") ? getAuthorsString(recommendation) : ""; + String title = isRecommendationFieldPresent(recommendation, "title") ? recommendation.getString("title") : ""; + String year = isRecommendationFieldPresent(recommendation, "year_published") ? Integer.toString(recommendation.getInt("year_published")) : ""; + String journal = isRecommendationFieldPresent(recommendation, "published_in") ? recommendation.getString("published_in") : ""; + String url = isRecommendationFieldPresent(recommendation, "url") ? recommendation.getString("url") : ""; + Integer rank = isRecommendationFieldPresent(recommendation, "url") ? recommendation.getInt("recommendation_id") : 100; + + // Populate bib entry with relevant data + current.setField(FieldName.AUTHOR, authors); + current.setField(FieldName.TITLE, title); + current.setField(FieldName.YEAR, year); + current.setField(FieldName.JOURNAL, journal); + current.setField(FieldName.URL, url); + + return new RankedBibEntry(current, rank); + } - @Override - public void startElement(String uri, String localName, String qName, Attributes attributes) - throws SAXException { + private Boolean isRecommendationFieldPresent(JSONObject recommendation, String field) { + return recommendation.has(field) && !recommendation.isNull(field); + } - switch (qName.toLowerCase(Locale.ROOT)) { - case "related_article": - currentEntry = new BibEntry(); - htmlSnippetSingle = null; - htmlSnippetSingleRank = -1; - break; - case "authors": - authors = true; - break; - case "published_in": - published_in = true; - break; - case "title": - title = true; - break; - case "year": - year = true; - break; - case "type": - type = true; - break; - case "suggested_rank": - rank = true; - break; - default: - break; - } - if (qName.equalsIgnoreCase("snippet") - && attributes.getValue(0).equalsIgnoreCase("html_fully_formatted")) { - snippet = true; - } + /** + * Creates an authors string from a JSON recommendation + * @param recommendation JSON Object recommendation from Mr. DLib + * @return A string of all authors, separated by commas and finished with a full stop. + */ + private String getAuthorsString(JSONObject recommendation) { + String authorsString = ""; + JSONArray array = recommendation.getJSONArray("authors"); + for (int i = 0; i < array.length(); ++i) { + authorsString += array.getString(i) + "; "; } - - @Override - public void endElement(String uri, String localName, String qName) throws SAXException { - if (qName.equalsIgnoreCase("related_article")) { - rankedBibEntries.add(new RankedBibEntry(currentEntry, htmlSnippetSingleRank)); - currentEntry = new BibEntry(); - } + int stringLength = authorsString.length(); + if (stringLength > 2) { + authorsString = authorsString.substring(0, stringLength - 2) + "."; } + return authorsString; + } - @Override - public void characters(char ch[], int start, int length) throws SAXException { + public ParserResult getParserResult() { + return parserResult; + } - if (authors) { - currentEntry.setField(FieldName.AUTHOR, new String(ch, start, length)); - authors = false; - } - if (published_in) { - currentEntry.setField(FieldName.JOURNAL, new String(ch, start, length)); - published_in = false; - } - if (title) { - currentEntry.setField(FieldName.TITLE, new String(ch, start, length)); - title = false; - } - if (year) { - currentEntry.setField(FieldName.YEAR, new String(ch, start, length)); - year = false; - } - if (rank) { - htmlSnippetSingleRank = Integer.parseInt(new String(ch, start, length)); - rank = false; - } - if (snippet) { - currentEntry.setField("html_representation", new String(ch, start, length)); - snippet = false; + /** + * Gets the error message to be returned if there has been an error in returning recommendations. + * Returns default error message if there is no message from Mr. DLib. + * @param response The response from the MDL server as a string. + * @return String error message to be shown to the user. + */ + public String getResponseErrorMessage(String response) { + try { + JSONObject jsonObject = new JSONObject(response); + if (!jsonObject.has("message")) { + return jsonObject.getString("message"); } - + } catch (JSONException ex) { + return DEFAULT_MRDLIB_ERROR_MESSAGE; } - + return DEFAULT_MRDLIB_ERROR_MESSAGE; } } diff --git a/src/main/java/org/jabref/logic/util/StandardFileType.java b/src/main/java/org/jabref/logic/util/StandardFileType.java index 5eb21456754..db7766e9811 100644 --- a/src/main/java/org/jabref/logic/util/StandardFileType.java +++ b/src/main/java/org/jabref/logic/util/StandardFileType.java @@ -36,6 +36,7 @@ public enum StandardFileType implements FileType { RTF("rtf"), SXC("sxc"), XML("xml"), + JSON("json"), XMP("xmp"), ZIP("zip"); diff --git a/src/main/java/org/jabref/preferences/JabRefPreferences.java b/src/main/java/org/jabref/preferences/JabRefPreferences.java index 45f724fc252..000a75a0c94 100644 --- a/src/main/java/org/jabref/preferences/JabRefPreferences.java +++ b/src/main/java/org/jabref/preferences/JabRefPreferences.java @@ -302,6 +302,7 @@ public class JabRefPreferences implements PreferencesService { public static final String NAME_FORMATER_KEY = "nameFormatterNames"; public static final String PUSH_TO_APPLICATION = "pushToApplication"; public static final String SHOW_RECOMMENDATIONS = "showRecommendations"; + public static final String ACCEPT_RECOMMENDATIONS = "acceptRecommendations"; public static final String VALIDATE_IN_ENTRY_EDITOR = "validateInEntryEditor"; // Dropped file handler public static final String DROPPEDFILEHANDLER_RENAME = "DroppedFileHandler_RenameFile"; @@ -570,6 +571,7 @@ private JabRefPreferences() { defaults.put(MERGE_ENTRIES_DIFF_MODE, 2); defaults.put(SHOW_RECOMMENDATIONS, Boolean.TRUE); + defaults.put(ACCEPT_RECOMMENDATIONS, Boolean.FALSE); defaults.put(VALIDATE_IN_ENTRY_EDITOR, Boolean.TRUE); defaults.put(EDITOR_EMACS_KEYBINDINGS, Boolean.FALSE); defaults.put(EDITOR_EMACS_KEYBINDINGS_REBIND_CA, Boolean.TRUE); @@ -900,6 +902,7 @@ public EntryEditorPreferences getEntryEditorPreferences() { getImportFormatPreferences(), getCustomTabFieldNames(), getBoolean(SHOW_RECOMMENDATIONS), + getBoolean(ACCEPT_RECOMMENDATIONS), getBoolean(DEFAULT_SHOW_SOURCE), getBibtexKeyPatternPreferences(), Globals.getKeyPrefs(), diff --git a/src/main/resources/l10n/JabRef_en.properties b/src/main/resources/l10n/JabRef_en.properties index 529922c4b94..aa3101e4371 100644 --- a/src/main/resources/l10n/JabRef_en.properties +++ b/src/main/resources/l10n/JabRef_en.properties @@ -31,9 +31,9 @@ Accept=Accept Accept\ change=Accept change -Action=Action +Accept\ recommendations\ from\ Mr.\ DLib=Accept recommendations from Mr. DLib -What\ is\ Mr.\ DLib?=What is Mr. DLib? +Action=Action Add=Add @@ -379,6 +379,8 @@ Error\ occurred\ when\ parsing\ entry=Error occurred when parsing entry Error\ opening\ file=Error opening file +Error\ while\ fetching\ from\ Mr.DLib.=Error while fetching from Mr.DLib. + Error\ while\ writing=Error while writing '%0'\ exists.\ Overwrite\ file?='%0' exists. Overwrite file? @@ -466,6 +468,8 @@ found\ in\ AUX\ file=found in AUX file Full\ name=Full name +Further\ information\ about\ Mr\ DLib.\ for\ JabRef\ users.=Further information about Mr DLib. for JabRef users. + General=General General\ Fields=General Fields @@ -560,6 +564,8 @@ Independent\ group\:\ When\ selected,\ view\ only\ this\ group's\ entries=Indepe Work\ options=Work options +I\ Agree=I Agree + Insert=Insert Insert\ rows=Insert rows @@ -656,6 +662,9 @@ Move\ up=Move up Moved\ group\ "%0".=Moved group "%0". +Mr.\ DLib\ is\ an\ external\ service\ which\ provides\ article\ recommendations\ based\ on\ the\ currently\ selected\ entry.\ Data\ about\ the\ selected\ entry\ must\ be\ sent\ to\ Mr.\ DLib\ in\ order\ to\ provide\ these\ recommendations.\ Do\ you\ agree\ that\ this\ data\ may\ be\ sent?=Mr. DLib is an external service which provides article recommendations based on the currently selected entry. Data about the selected entry must be sent to Mr. DLib in order to provide these recommendations. Do you agree that this data may be sent? + + Name=Name Name\ formatter=Name formatter @@ -786,6 +795,8 @@ Please\ enter\ the\ string's\ label=Please enter the string's label Please\ select\ an\ importer.=Please select an importer. +Please\ restart\ JabRef\ for\ preferences\ to\ take\ effect.=Please restart JabRef for preferences to take effect. + Possible\ duplicate\ entries=Possible duplicate entries Possible\ duplicate\ of\ existing\ entry.\ Click\ to\ resolve.=Possible duplicate of existing entry. Click to resolve. @@ -897,6 +908,9 @@ Resolve\ strings\ for\ all\ fields\ except=Resolve strings for all fields except Resolve\ strings\ for\ standard\ BibTeX\ fields\ only=Resolve strings for standard BibTeX fields only resolved=resolved + +Restart=Restart + Review=Review Review\ changes=Review changes Review\ Field\ Migration=Review Field Migration @@ -1063,6 +1077,8 @@ This\ operation\ requires\ all\ selected\ entries\ to\ have\ BibTeX\ keys\ defin This\ operation\ requires\ one\ or\ more\ entries\ to\ be\ selected.=This operation requires one or more entries to be selected. +This\ setting\ may\ be\ changed\ in\ preferences\ at\ any\ time.=This setting may be changed in preferences at any time. + Toggle\ entry\ preview=Toggle entry preview Toggle\ groups\ interface=Toggle groups interface diff --git a/src/test/java/org/jabref/logic/importer/fetcher/MrDLibFetcherTest.java b/src/test/java/org/jabref/logic/importer/fetcher/MrDLibFetcherTest.java index c35684edf48..357e50393fd 100644 --- a/src/test/java/org/jabref/logic/importer/fetcher/MrDLibFetcherTest.java +++ b/src/test/java/org/jabref/logic/importer/fetcher/MrDLibFetcherTest.java @@ -3,6 +3,7 @@ import java.util.List; import org.jabref.logic.importer.FetcherException; +import org.jabref.logic.util.Version; import org.jabref.model.entry.BibEntry; import org.jabref.model.entry.FieldName; import org.jabref.testutils.category.FetcherTest; @@ -20,7 +21,7 @@ public class MrDLibFetcherTest { @BeforeEach public void setUp() { - fetcher = new MrDLibFetcher("", ""); + fetcher = new MrDLibFetcher("", Version.parse("")); } @Test diff --git a/src/test/java/org/jabref/logic/importer/fileformat/MrDLibImporterTest.java b/src/test/java/org/jabref/logic/importer/fileformat/MrDLibImporterTest.java index 7c2c1e5d837..5af07b4dfa3 100644 --- a/src/test/java/org/jabref/logic/importer/fileformat/MrDLibImporterTest.java +++ b/src/test/java/org/jabref/logic/importer/fileformat/MrDLibImporterTest.java @@ -19,23 +19,18 @@ public class MrDLibImporterTest { private MrDLibImporter importer; - private BufferedReader inputMin; - private BufferedReader inputMax; + private BufferedReader input; @BeforeEach public void setUp() { importer = new MrDLibImporter(); - - String testMin = ""; - String testMax = "https://api-dev.mr-dlib.org/v1/recommendations/204944/original_url?access_key=99ab2fc64f3228ab839e9e3525ac37f8&format=direct_url_forward02.02.033221.0http://sowiport.gesis.org/search/id/gesis-solis-00538797Fachhochschulverl.Gesundheit von Arbeitslosen fördern!: ein Handbuch für Wissenschaft und Praxis. . Fachhochschulverl.. 2009.]]>Gesundheit von Arbeitslosen fördern!: ein Handbuch für Wissenschaft und Praxis.. Fachhochschulverl.. 2009.]]>Gesundheit von Arbeitslosen fördern!: ein Handbuch für Wissenschaft und Praxis. . Fachhochschulverl.. 2009]]>2Gesundheit von Arbeitslosen fördern!: ein Handbuch für Wissenschaft und Praxis2009"; - testMax = testMax.replaceAll("&", ""); - inputMin = new BufferedReader(new StringReader(testMin)); - inputMax = new BufferedReader(new StringReader(testMax)); + String testInput = "{ \"label\": { \"label-language\": \"en\", \"label-text\": \"Related Items\" }, \"recommendation-set-id\": \"1\", \"recommendations\": { \"74021358\": { \"abstract\": \"abstract\", \"authors\": [ \"Sajovic, Marija\" ], \"year_published\": \"2006\", \"item_id_original\": \"12088644\", \"keywords\": [ \"visoko\\u0161olski program Geodezija - smer Prostorska informatika\" ], \"language_provided\": \"sl\", \"recommendation_id\": \"1\", \"title\": \"The protection of rural lands with the spatial development strategy on the case of Hrastnik commune\", \"url\": \"http://drugg.fgg.uni-lj.si/701/1/GEV_0199_Sajovic.pdf\" }, \"82005804\": { \"abstract\": \"abstract\", \"year_published\": null, \"item_id_original\": \"30145702\", \"language_provided\": null, \"recommendation_id\": \"2\", \"title\": \"Engagement of the volunteers in the solution to the accidents in the South-Moravia region\" }, \"82149599\": { \"abstract\": \"abstract\", \"year_published\": null, \"item_id_original\": \"97690763\", \"language_provided\": null, \"recommendation_id\": \"3\", \"title\": \"\\\"The only Father's word\\\". The relationship of the Father and the Son in the documents of saint John of the Cross\", \"url\": \"http://www.nusl.cz/ntk/nusl-285711\" }, \"84863921\": { \"abstract\": \"abstract\", \"authors\": [ \"Kaffa, Elena\" ], \"year_published\": null, \"item_id_original\": \"19397104\", \"keywords\": [ \"BX\", \"D111\" ], \"language_provided\": \"en\", \"recommendation_id\": \"4\", \"title\": \"Greek Church of Cyprus, the Morea and Constantinople during the Frankish Era (1196-1303)\" }, \"88950992\": { \"abstract\": \"abstract\", \"authors\": [ \"Yasui, Kono\" ], \"year_published\": null, \"item_id_original\": \"38763657\", \"language_provided\": null, \"recommendation_id\": \"5\", \"title\": \"A Phylogenetic Consideration on the Vascular Plants, Cotyledonary Node Including Hypocotyl Being Taken as the Ancestral Form : A Preliminary Note\" } }}"; + input = new BufferedReader(new StringReader(testInput)); } @Test public void testGetDescription() { - assertEquals("Takes valid xml documents. Parses from MrDLib API a BibEntry", importer.getDescription()); + assertEquals("Takes valid JSON documents from the Mr. DLib API and parses them into a BibEntry", importer.getDescription()); } @Test @@ -45,46 +40,35 @@ public void testGetName() { @Test public void testGetFileExtention() { - assertEquals(StandardFileType.XML, importer.getFileType()); - } - - @Test - public void testImportDatabaseIsHtmlSetCorrectly() throws IOException { - ParserResult parserResult = importer.importDatabase(inputMax); - - List resultList = parserResult.getDatabase().getEntries(); - - assertEquals( - "Gesundheit von Arbeitslosen fördern!: ein Handbuch für Wissenschaft und Praxis.. Fachhochschulverl.. 2009.", - resultList.get(0).getField("html_representation").get()); + assertEquals(StandardFileType.JSON, importer.getFileType()); } @Test public void testImportDatabaseIsYearSetCorrectly() throws IOException { - ParserResult parserResult = importer.importDatabase(inputMax); + ParserResult parserResult = importer.importDatabase(input); List resultList = parserResult.getDatabase().getEntries(); - assertEquals("2009", + assertEquals("2006", resultList.get(0).getLatexFreeField(FieldName.YEAR).get()); } @Test public void testImportDatabaseIsTitleSetCorrectly() throws IOException { - ParserResult parserResult = importer.importDatabase(inputMax); + ParserResult parserResult = importer.importDatabase(input); List resultList = parserResult.getDatabase().getEntries(); - assertEquals("Gesundheit von Arbeitslosen fördern!: ein Handbuch für Wissenschaft und Praxis", + assertEquals("The protection of rural lands with the spatial development strategy on the case of Hrastnik commune", resultList.get(0).getLatexFreeField(FieldName.TITLE).get()); } @Test public void testImportDatabaseMin() throws IOException { - ParserResult parserResult = importer.importDatabase(inputMin); + ParserResult parserResult = importer.importDatabase(input); List resultList = parserResult.getDatabase().getEntries(); - assertSame(0, resultList.size()); + assertSame(5, resultList.size()); } }