Skip to content

Add a new field for citation count #13531

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 42 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
dcc6076
first changes
SalvadorRomo Jul 8, 2025
1a76c57
partial change
SalvadorRomo Jul 8, 2025
aaef2ce
add fields to GUI
SalvadorRomo Jul 11, 2025
150bc2c
finish change
SalvadorRomo Jul 12, 2025
fe6b48d
fix citation count identifier
SalvadorRomo Jul 12, 2025
23fbd53
adding tests
SalvadorRomo Jul 12, 2025
b37475d
Merge branch 'JabRef:main' into fix-for-issue-13477
SalvadorRomo Jul 12, 2025
f4fa327
add more testing
SalvadorRomo Jul 12, 2025
1193823
merge files
SalvadorRomo Jul 12, 2025
44de54c
remove unwanted logic
SalvadorRomo Jul 12, 2025
05aa416
add entry to change log
SalvadorRomo Jul 12, 2025
15baed1
resolve comments
SalvadorRomo Jul 12, 2025
19923b8
resolve comments
SalvadorRomo Jul 12, 2025
db2ffd2
resolve conflicts
SalvadorRomo Jul 12, 2025
cd836e7
enhance citation count change log
SalvadorRomo Jul 12, 2025
a90024c
enhance citation count change log
SalvadorRomo Jul 12, 2025
9c3facb
change assertion style
SalvadorRomo Jul 12, 2025
84af44f
resolve conflicts
SalvadorRomo Jul 12, 2025
1d265d3
resolve conflicts
SalvadorRomo Jul 12, 2025
8bfc988
resolve conflicts
SalvadorRomo Jul 12, 2025
83709c9
resolve test errors
SalvadorRomo Jul 12, 2025
4333732
resolve conflicts
SalvadorRomo Jul 12, 2025
18f9de0
fix branch
SalvadorRomo Jul 13, 2025
91c8c62
fix test
SalvadorRomo Jul 13, 2025
0cf3850
remove unused class
SalvadorRomo Jul 13, 2025
e4fdd97
merge incoming changes
SalvadorRomo Jul 13, 2025
e1928d8
resolve comment
SalvadorRomo Jul 13, 2025
18e51bd
Update CHANGELOG.md
SalvadorRomo Jul 14, 2025
d12e8e4
resolve comments
SalvadorRomo Jul 14, 2025
1f7f5cf
merge master
SalvadorRomo Jul 14, 2025
ba09528
fix comments
SalvadorRomo Jul 14, 2025
25e3649
fix comments
SalvadorRomo Jul 14, 2025
4e0c1af
fix comments
SalvadorRomo Jul 14, 2025
1accb7c
fix issues
SalvadorRomo Jul 14, 2025
85190f9
Merge branch 'main' into fix-for-issue-13477
SalvadorRomo Jul 14, 2025
371f9f6
Merge branch 'main' into fix-for-issue-13477
SalvadorRomo Jul 15, 2025
3074fde
Merge branch 'main' into fix-for-issue-13477
koppor Jul 15, 2025
41deb2b
fix comments
SalvadorRomo Jul 15, 2025
fa1297f
remove unused import
SalvadorRomo Jul 15, 2025
4f1250d
fix comments
SalvadorRomo Jul 16, 2025
49d6d47
fix test cases
SalvadorRomo Jul 16, 2025
827b4a7
Merge branch 'main' into fix-for-issue-13477
SalvadorRomo Jul 16, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv

### Added

- We added a new citationCount field on the General tab, in order to track this value for a given paper. [#13477](https://github.com/JabRef/jabref/issues/13477)
- 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)
- We distribute arm64 images for Linux. [#10842](https://github.com/JabRef/jabref/issues/10842)
- When adding an entry to a library, a warning is displayed if said entry already exists in an active library. [#13261](https://github.com/JabRef/jabref/issues/13261)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package org.jabref.gui.fieldeditors;

import java.util.Optional;

import javax.swing.undo.UndoManager;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using Swing components in a JavaFX application violates the architectural decision to use only JavaFX. The UndoManager should be replaced with a JavaFX-specific implementation.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JabRef uses this - which is OK.


import javafx.fxml.FXML;
import javafx.scene.Parent;
import javafx.scene.control.Button;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.HBox;

import org.jabref.gui.DialogService;
import org.jabref.gui.StateManager;
import org.jabref.gui.autocompleter.SuggestionProvider;
import org.jabref.gui.fieldeditors.contextmenu.DefaultMenu;
import org.jabref.gui.preferences.GuiPreferences;
import org.jabref.logic.citation.SearchCitationsRelationsService;
import org.jabref.logic.integrity.FieldCheckers;
import org.jabref.logic.l10n.Localization;
import org.jabref.logic.util.TaskExecutor;
import org.jabref.model.entry.BibEntry;
import org.jabref.model.entry.field.Field;

import com.airhacks.afterburner.injection.Injector;
import com.airhacks.afterburner.views.ViewLoader;
import jakarta.inject.Inject;

public class CitationCountEditor extends HBox implements FieldEditorFX {
@FXML private CitationCountEditorViewModel viewModel;
@FXML private EditorTextField textField;
@FXML private Button fetchCitationCountButton;

@Inject private DialogService dialogService;
@Inject private GuiPreferences preferences;
@Inject private UndoManager undoManager;
@Inject private TaskExecutor taskExecutor;
@Inject private StateManager stateManager;
@Inject private SearchCitationsRelationsService searchCitationsRelationsService;

private Optional<BibEntry> entry = Optional.empty();
public CitationCountEditor(Field field,
SuggestionProvider<?> suggestionProvider,
FieldCheckers fieldCheckers) {
Injector.registerExistingAndInject(this);
this.viewModel = new CitationCountEditorViewModel(
field,
suggestionProvider,
fieldCheckers,
taskExecutor,
dialogService,
undoManager,
stateManager,
preferences,
searchCitationsRelationsService);

ViewLoader.view(this)
.root(this)
.load();

textField.textProperty().bindBidirectional(viewModel.textProperty());

fetchCitationCountButton.setTooltip(
new Tooltip(Localization.lang("Look up %0", field.getDisplayName())));
textField.initContextMenu(new DefaultMenu(textField), preferences.getKeyBindingRepository());
new EditorValidator(preferences).configureValidation(viewModel.getFieldValidator().getValidationStatus(), textField);
}

@FXML
private void fetchCitationCount() {
this.entry.ifPresent(viewModel::getCitationCount);
}

public CitationCountEditorViewModel getViewModel() {
return viewModel;
}

@Override
public void bindToEntry(BibEntry entry) {
this.entry = Optional.of(entry);
viewModel.bindToEntry(entry);
}

@Override
public Parent getNode() {
return this;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package org.jabref.gui.fieldeditors;

import java.util.Optional;

import javax.swing.undo.UndoManager;

import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;

import org.jabref.gui.DialogService;
import org.jabref.gui.StateManager;
import org.jabref.gui.autocompleter.SuggestionProvider;
import org.jabref.gui.preferences.GuiPreferences;
import org.jabref.logic.citation.SearchCitationsRelationsService;
import org.jabref.logic.integrity.FieldCheckers;
import org.jabref.logic.util.BackgroundTask;
import org.jabref.logic.util.TaskExecutor;
import org.jabref.model.entry.BibEntry;
import org.jabref.model.entry.field.Field;

public class CitationCountEditorViewModel extends AbstractEditorViewModel {
protected final BooleanProperty fetchCitationCountInProgress = new SimpleBooleanProperty(false);
private final TaskExecutor taskExecutor;
private final DialogService dialogService;
private final UndoManager undoManager;
private final StateManager stateManager;
private final GuiPreferences preferences;
private final SearchCitationsRelationsService searchCitationsRelationsService;
public CitationCountEditorViewModel(
Field field,
SuggestionProvider<?> suggestionProvider,
FieldCheckers fieldCheckers,
TaskExecutor taskExecutor,
DialogService dialogService,
UndoManager undoManager,
StateManager stateManager,
GuiPreferences preferences,
SearchCitationsRelationsService searchCitationsRelationsService) {
super(field, suggestionProvider, fieldCheckers, undoManager);
this.taskExecutor = taskExecutor;
this.dialogService = dialogService;
this.undoManager = undoManager;
this.stateManager = stateManager;
this.preferences = preferences;
this.searchCitationsRelationsService = searchCitationsRelationsService;
}

public BooleanProperty fetchCitationCountInProgressProperty() {
return fetchCitationCountInProgress;
}

public boolean getFetchCitationCountInProgress() {
return fetchCitationCountInProgress.get();
}

public void getCitationCount(BibEntry bibEntry) {
Optional<String> fieldAux = entry.getField(field);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

aux is very strange. Maybe name it fieldContent

BackgroundTask.wrap(() -> searchCitationsRelationsService.getCitationCount(bibEntry, fieldAux))
.onRunning(() -> fetchCitationCountInProgress.setValue(true))
.onFinished(() -> fetchCitationCountInProgress.setValue(false))
.onSuccess(identifier -> {
entry.setField(field, String.valueOf(identifier));
}).executeWith(taskExecutor);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ public static FieldEditorFX getForField(final Field field,
} else if (fieldProperties.contains(FieldProperty.IDENTIFIER) && field != StandardField.PMID || field == StandardField.ISBN) {
// Identifier editor does not support PMID, therefore excluded at the condition above
return new IdentifierEditor(field, suggestionProvider, fieldCheckers);
} else if (field == StandardField.CITATIONCOUNT) {
return new CitationCountEditor(field, suggestionProvider, fieldCheckers);
} else if (field == StandardField.ISSN) {
return new ISSNEditor(field, suggestionProvider, fieldCheckers, undoAction, redoAction);
} else if (field == StandardField.OWNER) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ public class IdentifierEditor extends HBox implements FieldEditorFX {
@Inject private GuiPreferences preferences;
@Inject private UndoManager undoManager;
@Inject private StateManager stateManager;

private Optional<BibEntry> entry = Optional.empty();

public IdentifierEditor(Field field,
Expand All @@ -63,6 +62,7 @@ public IdentifierEditor(Field field,
this.viewModel = new ISBNIdentifierEditorViewModel(suggestionProvider, fieldCheckers, dialogService, taskExecutor, preferences, undoManager, stateManager);
case EPRINT ->
this.viewModel = new EprintIdentifierEditorViewModel(suggestionProvider, fieldCheckers, dialogService, taskExecutor, preferences, undoManager);

// TODO: Add support for PMID
case null, default -> {
assert field != null;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.control.Button?>
<?import javafx.scene.control.ProgressIndicator?>
<?import javafx.scene.control.Tooltip?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.StackPane?>
<?import org.jabref.gui.fieldeditors.EditorTextField?>
<?import org.jabref.gui.icon.JabRefIconView?>
<fx:root xmlns:fx="http://javafx.com/fxml/1" type="HBox" xmlns="http://javafx.com/javafx/8.0.112"
fx:controller="org.jabref.gui.fieldeditors.CitationCountEditor">
<EditorTextField fx:id="textField" prefHeight="0.0" HBox.hgrow="ALWAYS"/>
<Button fx:id="fetchCitationCountButton" onAction="#fetchCitationCount" styleClass="icon-button">
<graphic>
<StackPane>
<JabRefIconView glyph="LOOKUP_IDENTIFIER"
visible="${controller.viewModel.fetchCitationCountInProgress == false}"/>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use !... instead of == false

<ProgressIndicator maxHeight="12.0" maxWidth="12.0"
visible="${controller.viewModel.fetchCitationCountInProgress}"/>
</StackPane>
</graphic>
</Button>
</fx:root>
3 changes: 2 additions & 1 deletion jablib/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -421,7 +421,8 @@ tasks.test {
jvmArgs = listOf(
"-javaagent:${mockitoAgent.asPath}",
"--add-opens", "java.base/jdk.internal.ref=org.apache.pdfbox.io",
"--add-opens", "java.base/java.nio=org.apache.pdfbox.io"
"--add-opens", "java.base/java.nio=org.apache.pdfbox.io",
"--add-reads", "org.xmlunit.matchers=java.xml"
)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.jabref.logic.citation;

import java.util.List;
import java.util.Optional;

import org.jabref.logic.bibtex.FieldPreferences;
import org.jabref.logic.citation.repository.BibEntryCitationsAndReferencesRepository;
Expand All @@ -9,6 +10,7 @@
import org.jabref.logic.importer.ImportFormatPreferences;
import org.jabref.logic.importer.ImporterPreferences;
import org.jabref.logic.importer.fetcher.citation.CitationFetcher;
import org.jabref.logic.importer.fetcher.citation.semanticscholar.PaperDetails;
import org.jabref.logic.importer.fetcher.citation.semanticscholar.SemanticScholarCitationFetcher;
import org.jabref.logic.util.Directories;
import org.jabref.model.entry.BibEntry;
Expand Down Expand Up @@ -83,6 +85,21 @@ public List<BibEntry> searchCitations(BibEntry cited) {
return relationsRepository.readCitations(cited);
}

public int getCitationCount(BibEntry citationCounted, Optional<String> actualFieldValue) {
Optional<PaperDetails> citationCountResult = null;
boolean isFetchingAllowed = relationsRepository.isCitationsUpdatable(citationCounted)
|| actualFieldValue.isEmpty();
if (isFetchingAllowed) {
try {
citationCountResult = citationFetcher.searchCitationCount(citationCounted);
} catch (FetcherException e) {
LOGGER.error("Error while fetching citation count for entry", e);
}
return citationCountResult.map(PaperDetails::getCitationCount).orElse(0);
}
return Integer.parseInt(actualFieldValue.get());
}

public void close() {
relationsRepository.close();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package org.jabref.logic.importer.fetcher.citation;

import java.util.List;
import java.util.Optional;

import org.jabref.logic.importer.FetcherException;
import org.jabref.logic.importer.fetcher.citation.semanticscholar.PaperDetails;
import org.jabref.model.entry.BibEntry;

/**
Expand Down Expand Up @@ -40,6 +42,14 @@ enum SearchType {
*/
List<BibEntry> searchCiting(BibEntry entry) throws FetcherException;

/**
* Get the paper details that includes citation count field for a given {@link BibEntry}.
*
* @param entry entry to search citation count field
* @return returns a {@link PaperDetails}, that includes citation count field (may be empty)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ajdust JavaDoc to actual return value (Integer is not PaperDetails)

*/
Optional<PaperDetails> searchCitationCount(BibEntry entry) throws FetcherException;

/**
* Returns the localized name of this fetcher.
* The title can be used to display the fetcher in the menu and in the side pane.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import java.net.MalformedURLException;
import java.net.URL;
import java.util.List;
import java.util.Optional;

import org.jabref.logic.importer.FetcherException;
import org.jabref.logic.importer.ImporterPreferences;
Expand Down Expand Up @@ -33,6 +34,12 @@ public String getAPIUrl(String entry_point, BibEntry entry) {
+ "&limit=1000";
}

public String getAPIUrl(BibEntry entry) {
return SEMANTIC_SCHOLAR_API + "paper/" + "DOI:" + entry.getDOI().orElseThrow().asString()
+ "?fields=" + "title,authors,year,citationCount,referenceCount,externalIds,publicationTypes,abstract,url"
+ "&limit=1000";
}

@Override
public List<BibEntry> searchCitedBy(BibEntry entry) throws FetcherException {
if (entry.getDOI().isEmpty()) {
Expand Down Expand Up @@ -84,6 +91,27 @@ public List<BibEntry> searchCitedBy(BibEntry entry) throws FetcherException {
.map(referenceDataItem -> referenceDataItem.getCitedPaper().toBibEntry()).toList();
}

@Override
public Optional<PaperDetails> searchCitationCount(BibEntry entry) throws FetcherException {
if (entry.getDOI().isEmpty()) {
return Optional.empty();
}
URL referencesUrl;
try {
referencesUrl = URLUtil.create(getAPIUrl(entry));
} catch (MalformedURLException e) {
throw new FetcherException("Malformed URL", e);
}
URLDownload urlDownload = new URLDownload(referencesUrl);
importerPreferences.getApiKey(getName()).ifPresent(apiKey -> urlDownload.addHeader("x-api-key", apiKey));
PaperDetails referencesResponse = GSON.fromJson(urlDownload.asString(), PaperDetails.class);

if (referencesResponse == null) {
return Optional.empty();
}
return Optional.of(referencesResponse);
}

@Override
public String getName() {
return FETCHER_NAME;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ private static Set<Field> getAllFields() {
* separate preferences object
*/
public static List<Field> getDefaultGeneralFields() {
List<Field> defaultGeneralFields = new ArrayList<>(Arrays.asList(StandardField.DOI, StandardField.CROSSREF, StandardField.KEYWORDS, StandardField.EPRINT, StandardField.URL, StandardField.FILE, StandardField.GROUPS, StandardField.OWNER, StandardField.TIMESTAMP));
List<Field> defaultGeneralFields = new ArrayList<>(Arrays.asList(StandardField.DOI, StandardField.CITATIONCOUNT, StandardField.CROSSREF, StandardField.KEYWORDS, StandardField.EPRINT, StandardField.URL, StandardField.FILE, StandardField.GROUPS, StandardField.OWNER, StandardField.TIMESTAMP));
defaultGeneralFields.addAll(EnumSet.allOf(SpecialField.class));
return defaultGeneralFields;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ public enum FieldProperty {

// Field content should be treated as data
VERBATIM,
CITATION_COUNT,

YES_NO
}
Original file line number Diff line number Diff line change
Expand Up @@ -138,8 +138,10 @@ public enum StandardField implements Field {
OWNER("owner"),
TIMESTAMP("timestamp", FieldProperty.DATE),
CREATIONDATE("creationdate", FieldProperty.DATE),
CITATIONCOUNT("Citation count", FieldProperty.CITATION_COUNT),
MODIFICATIONDATE("modificationdate", FieldProperty.DATE);


public static final Set<Field> AUTOMATIC_FIELDS = Set.of(OWNER, TIMESTAMP, CREATIONDATE, MODIFICATIONDATE);

private static final Map<String, StandardField> NAME_TO_STANDARD_FIELD = new HashMap<>();
Expand Down
Loading