Skip to content

Commit d6858cb

Browse files
SalvadorRomokopporSalvador Romo
authored
Add a new field for citation count (#13531)
* first changes * partial change * add fields to GUI * finish change * fix citation count identifier * adding tests * add more testing * remove unwanted logic * add entry to change log * resolve comments * resolve comments * resolve conflicts * enhance citation count change log * enhance citation count change log * change assertion style * resolve conflicts * resolve conflicts * resolve conflicts * resolve test errors * resolve conflicts * fix branch * fix test * remove unused class * resolve comment * Update CHANGELOG.md Co-authored-by: Oliver Kopp <[email protected]> * resolve comments * fix comments * fix comments * fix comments * fix issues * fix comments * remove unused import * fix comments * fix test cases * fix comments * fix tests * Fix space in fieldname * Use "error" from server for notification of error * Fix requires for xmlunit * Ensure that streams are closed during test * Fix test file * Fix OpenRewrite --------- Co-authored-by: Oliver Kopp <[email protected]> Co-authored-by: Salvador Romo <[email protected]>
1 parent e1d0d66 commit d6858cb

File tree

19 files changed

+493
-140
lines changed

19 files changed

+493
-140
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv
1111

1212
### Added
1313

14+
- We added a field for the citation count field on the General tab. [#13477](https://github.com/JabRef/jabref/issues/13477)
1415
- We added automatic lookup of DOI at citation relations [#13234](https://github.com/JabRef/jabref/issues/13234)
1516
- We added focus on the field Link in the "Add file link" dialog. [#13486](https://github.com/JabRef/jabref/issues/13486)
1617
- We introduced a settings parameter to manage citations' relations local storage time-to-live with a default value set to 30 days. [#11189](https://github.com/JabRef/jabref/issues/11189)

build-logic/src/main/kotlin/org.jabref.gradle.base.dependency-rules.gradle.kts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -274,8 +274,9 @@ extraJavaModuleInfo {
274274
module("org.xmlunit:xmlunit-matchers", "org.xmlunit.matchers") {
275275
exportAllPackages()
276276
requires("java.logging")
277-
requires("org.xmlunit")
277+
requires("java.xml")
278278
requires("org.hamcrest")
279+
requires("org.xmlunit")
279280
}
280281
module("org.xmlunit:xmlunit-placeholders", "org.xmlunit.placeholder")
281282

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package org.jabref.gui.fieldeditors;
2+
3+
import javax.swing.undo.UndoManager;
4+
5+
import javafx.fxml.FXML;
6+
import javafx.scene.Parent;
7+
import javafx.scene.control.Button;
8+
import javafx.scene.control.Tooltip;
9+
import javafx.scene.layout.HBox;
10+
11+
import org.jabref.gui.DialogService;
12+
import org.jabref.gui.StateManager;
13+
import org.jabref.gui.autocompleter.SuggestionProvider;
14+
import org.jabref.gui.fieldeditors.contextmenu.DefaultMenu;
15+
import org.jabref.gui.preferences.GuiPreferences;
16+
import org.jabref.logic.citation.SearchCitationsRelationsService;
17+
import org.jabref.logic.integrity.FieldCheckers;
18+
import org.jabref.logic.l10n.Localization;
19+
import org.jabref.logic.util.TaskExecutor;
20+
import org.jabref.model.entry.BibEntry;
21+
import org.jabref.model.entry.field.Field;
22+
23+
import com.airhacks.afterburner.injection.Injector;
24+
import com.airhacks.afterburner.views.ViewLoader;
25+
import jakarta.inject.Inject;
26+
27+
public class CitationCountEditor extends HBox implements FieldEditorFX {
28+
@FXML private CitationCountEditorViewModel viewModel;
29+
@FXML private EditorTextField textField;
30+
@FXML private Button fetchCitationCountButton;
31+
32+
@Inject private DialogService dialogService;
33+
@Inject private GuiPreferences preferences;
34+
@Inject private UndoManager undoManager;
35+
@Inject private TaskExecutor taskExecutor;
36+
@Inject private StateManager stateManager;
37+
@Inject private SearchCitationsRelationsService searchCitationsRelationsService;
38+
39+
public CitationCountEditor(Field field,
40+
SuggestionProvider<?> suggestionProvider,
41+
FieldCheckers fieldCheckers) {
42+
Injector.registerExistingAndInject(this);
43+
this.viewModel = new CitationCountEditorViewModel(
44+
field,
45+
suggestionProvider,
46+
fieldCheckers,
47+
taskExecutor,
48+
dialogService,
49+
undoManager,
50+
stateManager,
51+
preferences,
52+
searchCitationsRelationsService);
53+
54+
ViewLoader.view(this)
55+
.root(this)
56+
.load();
57+
58+
textField.textProperty().bindBidirectional(viewModel.textProperty());
59+
60+
fetchCitationCountButton.setTooltip(
61+
new Tooltip(Localization.lang("Look up %0", field.getDisplayName())));
62+
textField.initContextMenu(new DefaultMenu(textField), preferences.getKeyBindingRepository());
63+
new EditorValidator(preferences).configureValidation(viewModel.getFieldValidator().getValidationStatus(), textField);
64+
}
65+
66+
@FXML
67+
private void fetchCitationCount() {
68+
viewModel.getCitationCount();
69+
}
70+
71+
public CitationCountEditorViewModel getViewModel() {
72+
return viewModel;
73+
}
74+
75+
@Override
76+
public void bindToEntry(BibEntry entry) {
77+
viewModel.bindToEntry(entry);
78+
}
79+
80+
@Override
81+
public Parent getNode() {
82+
return this;
83+
}
84+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package org.jabref.gui.fieldeditors;
2+
3+
import java.util.Optional;
4+
5+
import javax.swing.undo.UndoManager;
6+
7+
import javafx.beans.property.BooleanProperty;
8+
import javafx.beans.property.SimpleBooleanProperty;
9+
10+
import org.jabref.gui.DialogService;
11+
import org.jabref.gui.StateManager;
12+
import org.jabref.gui.autocompleter.SuggestionProvider;
13+
import org.jabref.gui.preferences.GuiPreferences;
14+
import org.jabref.logic.citation.SearchCitationsRelationsService;
15+
import org.jabref.logic.integrity.FieldCheckers;
16+
import org.jabref.logic.l10n.Localization;
17+
import org.jabref.logic.util.BackgroundTask;
18+
import org.jabref.logic.util.TaskExecutor;
19+
import org.jabref.model.entry.field.Field;
20+
21+
import org.slf4j.Logger;
22+
import org.slf4j.LoggerFactory;
23+
24+
public class CitationCountEditorViewModel extends AbstractEditorViewModel {
25+
private static final Logger LOGGER = LoggerFactory.getLogger(CitationCountEditorViewModel.class);
26+
27+
protected final BooleanProperty fetchCitationCountInProgress = new SimpleBooleanProperty(false);
28+
private final TaskExecutor taskExecutor;
29+
private final DialogService dialogService;
30+
private final UndoManager undoManager;
31+
private final StateManager stateManager;
32+
private final GuiPreferences preferences;
33+
private final SearchCitationsRelationsService searchCitationsRelationsService;
34+
public CitationCountEditorViewModel(
35+
Field field,
36+
SuggestionProvider<?> suggestionProvider,
37+
FieldCheckers fieldCheckers,
38+
TaskExecutor taskExecutor,
39+
DialogService dialogService,
40+
UndoManager undoManager,
41+
StateManager stateManager,
42+
GuiPreferences preferences,
43+
SearchCitationsRelationsService searchCitationsRelationsService) {
44+
super(field, suggestionProvider, fieldCheckers, undoManager);
45+
this.taskExecutor = taskExecutor;
46+
this.dialogService = dialogService;
47+
this.undoManager = undoManager;
48+
this.stateManager = stateManager;
49+
this.preferences = preferences;
50+
this.searchCitationsRelationsService = searchCitationsRelationsService;
51+
}
52+
53+
public BooleanProperty fetchCitationCountInProgressProperty() {
54+
return fetchCitationCountInProgress;
55+
}
56+
57+
public boolean getFetchCitationCountInProgress() {
58+
return fetchCitationCountInProgress.get();
59+
}
60+
61+
public void getCitationCount() {
62+
Optional<String> fieldContent = entry.getField(field);
63+
BackgroundTask.wrap(() -> searchCitationsRelationsService.getCitationCount(this.entry, fieldContent))
64+
.onRunning(() -> fetchCitationCountInProgress.setValue(true))
65+
.onFinished(() -> fetchCitationCountInProgress.setValue(false))
66+
.onFailure(e -> {
67+
dialogService.notify(Localization.lang("Error occurred when getting citation count, please try again or check the identifier.\n\n%0", e.getLocalizedMessage()));
68+
LOGGER.error("Error while fetching citation count", e);
69+
})
70+
.onSuccess(identifier -> {
71+
entry.setField(field, String.valueOf(identifier));
72+
}).executeWith(taskExecutor);
73+
}
74+
}

jabgui/src/main/java/org/jabref/gui/fieldeditors/FieldEditors.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@ public static FieldEditorFX getForField(final Field field,
7575
} else if (fieldProperties.contains(FieldProperty.IDENTIFIER) && field != StandardField.PMID || field == StandardField.ISBN) {
7676
// Identifier editor does not support PMID, therefore excluded at the condition above
7777
return new IdentifierEditor(field, suggestionProvider, fieldCheckers);
78+
} else if (field == StandardField.CITATIONCOUNT) {
79+
return new CitationCountEditor(field, suggestionProvider, fieldCheckers);
7880
} else if (field == StandardField.ISSN) {
7981
return new ISSNEditor(field, suggestionProvider, fieldCheckers, undoAction, redoAction);
8082
} else if (field == StandardField.OWNER) {

jabgui/src/main/java/org/jabref/gui/fieldeditors/identifier/IdentifierEditor.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@ public class IdentifierEditor extends HBox implements FieldEditorFX {
4545
@Inject private GuiPreferences preferences;
4646
@Inject private UndoManager undoManager;
4747
@Inject private StateManager stateManager;
48-
4948
private Optional<BibEntry> entry = Optional.empty();
5049

5150
public IdentifierEditor(Field field,
@@ -63,6 +62,7 @@ public IdentifierEditor(Field field,
6362
this.viewModel = new ISBNIdentifierEditorViewModel(suggestionProvider, fieldCheckers, dialogService, taskExecutor, preferences, undoManager, stateManager);
6463
case EPRINT ->
6564
this.viewModel = new EprintIdentifierEditorViewModel(suggestionProvider, fieldCheckers, dialogService, taskExecutor, preferences, undoManager);
65+
6666
// TODO: Add support for PMID
6767
case null, default -> {
6868
assert field != null;
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
3+
<?import javafx.scene.control.Button?>
4+
<?import javafx.scene.control.ProgressIndicator?>
5+
<?import javafx.scene.control.Tooltip?>
6+
<?import javafx.scene.layout.HBox?>
7+
<?import javafx.scene.layout.StackPane?>
8+
<?import org.jabref.gui.fieldeditors.EditorTextField?>
9+
<?import org.jabref.gui.icon.JabRefIconView?>
10+
<fx:root xmlns:fx="http://javafx.com/fxml/1" type="HBox" xmlns="http://javafx.com/javafx/8.0.112"
11+
fx:controller="org.jabref.gui.fieldeditors.CitationCountEditor">
12+
<EditorTextField fx:id="textField" prefHeight="0.0" HBox.hgrow="ALWAYS"/>
13+
<Button fx:id="fetchCitationCountButton" onAction="#fetchCitationCount" styleClass="icon-button">
14+
<graphic>
15+
<StackPane>
16+
<JabRefIconView glyph="LOOKUP_IDENTIFIER"
17+
visible="${!controller.viewModel.fetchCitationCountInProgress}"/>
18+
<ProgressIndicator maxHeight="12.0" maxWidth="12.0"
19+
visible="${controller.viewModel.fetchCitationCountInProgress}"/>
20+
</StackPane>
21+
</graphic>
22+
</Button>
23+
</fx:root>

jablib/src/main/java/org/jabref/logic/citation/SearchCitationsRelationsService.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package org.jabref.logic.citation;
22

33
import java.util.List;
4+
import java.util.Optional;
45

56
import org.jabref.logic.bibtex.FieldPreferences;
67
import org.jabref.logic.citation.repository.BibEntryCitationsAndReferencesRepository;
@@ -83,6 +84,17 @@ public List<BibEntry> searchCitations(BibEntry cited) {
8384
return relationsRepository.readCitations(cited);
8485
}
8586

87+
public int getCitationCount(BibEntry citationCounted, Optional<String> actualFieldValue) throws FetcherException {
88+
boolean isFetchingAllowed = actualFieldValue.isEmpty() ||
89+
relationsRepository.isCitationsUpdatable(citationCounted);
90+
if (isFetchingAllowed) {
91+
Optional<Integer> citationCountResult = citationFetcher.searchCitationCount(citationCounted);
92+
return citationCountResult.orElse(0);
93+
}
94+
assert actualFieldValue.isPresent();
95+
return Integer.parseInt(actualFieldValue.get());
96+
}
97+
8698
public void close() {
8799
relationsRepository.close();
88100
}

jablib/src/main/java/org/jabref/logic/importer/fetcher/citation/CitationFetcher.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package org.jabref.logic.importer.fetcher.citation;
22

33
import java.util.List;
4+
import java.util.Optional;
45

56
import org.jabref.logic.importer.FetcherException;
67
import org.jabref.model.entry.BibEntry;
@@ -40,6 +41,14 @@ enum SearchType {
4041
*/
4142
List<BibEntry> searchCiting(BibEntry entry) throws FetcherException;
4243

44+
/**
45+
* Get the paper details that includes citation count field for a given {@link BibEntry}.
46+
*
47+
* @param entry entry to search citation count field
48+
* @return returns a {@link Integer} for citation count field (may be empty)
49+
*/
50+
Optional<Integer> searchCitationCount(BibEntry entry) throws FetcherException;
51+
4352
/**
4453
* Returns the localized name of this fetcher.
4554
* The title can be used to display the fetcher in the menu and in the side pane.

jablib/src/main/java/org/jabref/logic/importer/fetcher/citation/semanticscholar/SemanticScholarCitationFetcher.java

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import java.net.MalformedURLException;
44
import java.net.URL;
55
import java.util.List;
6+
import java.util.Optional;
67

78
import org.jabref.logic.importer.FetcherException;
89
import org.jabref.logic.importer.ImporterPreferences;
@@ -13,6 +14,8 @@
1314
import org.jabref.model.entry.BibEntry;
1415

1516
import com.google.gson.Gson;
17+
import kong.unirest.core.json.JSONObject;
18+
import org.jooq.lambda.Unchecked;
1619
import org.jspecify.annotations.NonNull;
1720

1821
public class SemanticScholarCitationFetcher implements CitationFetcher, CustomizableKeyFetcher {
@@ -33,6 +36,12 @@ public String getAPIUrl(String entry_point, BibEntry entry) {
3336
+ "&limit=1000";
3437
}
3538

39+
public String getUrlForCitationCount(BibEntry entry) {
40+
return SEMANTIC_SCHOLAR_API + "paper/" + "DOI:" + entry.getDOI().orElseThrow().asString()
41+
+ "?fields=" + "citationCount"
42+
+ "&limit=1";
43+
}
44+
3645
@Override
3746
public List<BibEntry> searchCitedBy(BibEntry entry) throws FetcherException {
3847
if (entry.getDOI().isEmpty()) {
@@ -84,6 +93,41 @@ public List<BibEntry> searchCitedBy(BibEntry entry) throws FetcherException {
8493
.map(referenceDataItem -> referenceDataItem.getCitedPaper().toBibEntry()).toList();
8594
}
8695

96+
@Override
97+
public Optional<Integer> searchCitationCount(BibEntry entry) throws FetcherException {
98+
if (entry.getDOI().isEmpty()) {
99+
return Optional.empty();
100+
}
101+
URL referencesUrl;
102+
try {
103+
referencesUrl = URLUtil.create(getUrlForCitationCount(entry));
104+
} catch (MalformedURLException e) {
105+
throw new FetcherException("Malformed URL", e);
106+
}
107+
URLDownload urlDownload = new URLDownload(referencesUrl);
108+
importerPreferences.getApiKey(getName()).ifPresent(apiKey -> urlDownload.addHeader("x-api-key", apiKey));
109+
String result;
110+
try {
111+
result = urlDownload.asString();
112+
} catch (FetcherException e) {
113+
e.getHttpResponse().ifPresent(Unchecked.consumer(response -> {
114+
Optional.ofNullable(response.responseBody())
115+
.map(JSONObject::new)
116+
.flatMap(json -> Optional.ofNullable(json.getString("error"))
117+
.map(Unchecked.function(error -> {
118+
throw new FetcherException(referencesUrl, error, e);
119+
})));
120+
}));
121+
throw e;
122+
}
123+
PaperDetails paperDetails = GSON.fromJson(result, PaperDetails.class);
124+
125+
if (paperDetails == null) {
126+
return Optional.empty();
127+
}
128+
return Optional.of(paperDetails.getCitationCount());
129+
}
130+
87131
@Override
88132
public String getName() {
89133
return FETCHER_NAME;

0 commit comments

Comments
 (0)