Skip to content

Add ICORE Ranking support #13512

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 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -82,7 +82,8 @@
- We removed the ability to change internal preference values. [#13012](https://github.com/JabRef/jabref/pull/13012)
- We removed support for MySQL/MariaDB and Oracle. [#12990](https://github.com/JabRef/jabref/pull/12990)
- We removed library migrations (users need to use JabRef 6.0-alpha.1 to perform migrations) [#12990](https://github.com/JabRef/jabref/pull/12990)

Check failure on line 85 in CHANGELOG.md

View workflow job for this annotation

GitHub Actions / Markdown

Multiple consecutive blank lines

CHANGELOG.md:85 MD012/no-multiple-blanks Multiple consecutive blank lines [Expected: 1; Actual: 2] https://github.com/DavidAnson/markdownlint/blob/v0.38.0/doc/md012.md

## [6.0-alpha2] – 2025-04-27

### Added
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ public static FieldEditorFX getForField(final Field field,
MonthEditorViewModel(field, suggestionProvider, databaseContext.getMode(), fieldCheckers, undoManager));
} else if (fieldProperties.contains(FieldProperty.LANGUAGE)) {
return new OptionEditor<>(new LanguageEditorViewModel(field, suggestionProvider, databaseContext.getMode(), fieldCheckers, undoManager));
} else if (field == StandardField.ICORERANKING) {
return new ICoreRankingEditor(field);
} else if (field == StandardField.GENDER) {
return new OptionEditor<>(new GenderEditorViewModel(field, suggestionProvider, fieldCheckers, undoManager));
} else if (fieldProperties.contains(FieldProperty.EDITOR_TYPE)) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package org.jabref.gui.fieldeditors;

import java.util.Optional;

import javafx.beans.property.StringProperty;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.control.Button;
import javafx.scene.control.TextField;
import javafx.scene.control.TextInputControl;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.HBox;
import org.jabref.gui.icon.IconTheme;
import org.jabref.gui.keyboard.KeyBindingRepository;
import org.jabref.gui.undo.RedoAction;
import org.jabref.gui.undo.UndoAction;
import org.jabref.logic.icore.ConferenceRankingEntry;
import org.jabref.logic.icore.ICoreRankingRepository;
import org.jabref.logic.util.ConferenceUtil;
import org.jabref.model.entry.BibEntry;
import org.jabref.model.entry.field.Field;
import org.jabref.model.entry.field.StandardField;

public class ICoreRankingEditor extends HBox implements FieldEditorFX {

private final Field field;
private final TextField textField;
private BibEntry currentEntry;
private final ICoreRankingRepository repo;

public ICoreRankingEditor(Field field) {
this.field = field;
this.textField = new TextField();
this.repo = new ICoreRankingRepository(); // Load once

this.textField.setPromptText("Enter or lookup ICORE rank");

// Button lookupButton = new Button("Lookup Rank");
Copy link

Choose a reason for hiding this comment

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

Commented code should be removed as it serves no purpose and clutters the codebase. Version control should be used to track code history instead.

Button lookupButton = new Button();
lookupButton.getStyleClass().setAll("icon-button");
lookupButton.setGraphic(IconTheme.JabRefIcons.LOOKUP_IDENTIFIER.getGraphicNode());
lookupButton.setTooltip(new Tooltip("Look up Icore Rank"));
lookupButton.setOnAction(event -> lookupRank());
this.getChildren().addAll(textField, lookupButton);
this.setSpacing(10);

lookupButton.setOnAction(event -> lookupRank());
}

private void lookupRank() {
if (currentEntry == null) {
return;
}

Optional<String> icoreField = currentEntry.getField(StandardField.ICORERANKING);
Optional<String> bookTitle = currentEntry.getFieldOrAlias(StandardField.BOOKTITLE);
if (bookTitle.isEmpty()) {
bookTitle = currentEntry.getField(StandardField.JOURNAL);
Copy link
Member

Choose a reason for hiding this comment

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

also getFieldOrAlias

}

Optional<String> finalBookTitle = bookTitle;
Copy link
Member

Choose a reason for hiding this comment

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

What is this?

String rawInput = icoreField.orElseGet(() -> finalBookTitle.orElse("Unknown"));

Optional<String> acronym = ConferenceUtil.extractAcronym(rawInput); // Extracting the acronym from our input field
Optional<ConferenceRankingEntry> result = acronym.flatMap(repo::getFullEntry)
.or(() -> repo.getFullEntry(rawInput));

if (result.isPresent()) {
ConferenceRankingEntry entry = result.get();

// Show in new dialog
javafx.scene.control.Alert alert = new javafx.scene.control.Alert(javafx.scene.control.Alert.AlertType.INFORMATION);
Copy link
Member

Choose a reason for hiding this comment

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

I think, this is for debugging and can be removed?

alert.setTitle("ICORE Ranking Info");
alert.setHeaderText("Found Conference Details");
Comment on lines +73 to +74
Copy link

Choose a reason for hiding this comment

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

UI text uses title case instead of sentence case as required by project guidelines for consistency in the user interface.

alert.setContentText(entry.toString());
alert.setResizable(true);
alert.getDialogPane().setPrefSize(600, 400);
alert.showAndWait();

textField.setText(entry.rank()); // still show rank in the field
} else {
textField.setText("Not ranked");
}
}

@Override
public void bindToEntry(BibEntry entry) {
this.currentEntry = entry;
entry.getField(field).ifPresent(textField::setText);

textField.textProperty().addListener((obs, oldVal, newVal) -> {
entry.setField(field, newVal);
});
}

@Override
public void establishBinding(TextInputControl textInputControl, StringProperty viewModelTextProperty,
KeyBindingRepository keyBindingRepository, UndoAction undoAction, RedoAction redoAction) {
FieldEditorFX.super.establishBinding(textInputControl, viewModelTextProperty, keyBindingRepository, undoAction, redoAction);
}

@Override
public Parent getNode() {
return this;
}

@Override
public void focus() {
FieldEditorFX.super.focus();
}

@Override
public double getWeight() {
return FieldEditorFX.super.getWeight();
}

@Override
public void requestFocus() {
textField.requestFocus();
}

@Override
public Node getStyleableNode() {
return super.getStyleableNode();
}
}
Original file line number Diff line number Diff line change
@@ -1,18 +1,13 @@
package org.jabref.migrations;

import java.util.ArrayList;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.*;
Copy link

Choose a reason for hiding this comment

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

Using wildcard imports instead of specific imports reduces code readability and can lead to naming conflicts. Each import should be explicitly declared.

import java.util.function.UnaryOperator;
import java.util.prefs.BackingStoreException;
import java.util.prefs.Preferences;
import java.util.stream.Collectors;

import com.github.javakeyring.Keyring;
import javafx.scene.control.TableColumn;

import org.jabref.gui.entryeditor.CommentsTab;
import org.jabref.gui.maintable.ColumnPreferences;
import org.jabref.gui.maintable.MainTableColumnModel;
Expand All @@ -29,16 +24,16 @@
import org.jabref.model.entry.field.StandardField;
import org.jabref.model.entry.types.EntryTypeFactory;
import org.jabref.model.strings.StringUtil;

import com.github.javakeyring.Keyring;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class PreferencesMigrations {

private static final Logger LOGGER = LoggerFactory.getLogger(PreferencesMigrations.class);
private JabRefGuiPreferences preferences;
Copy link

Choose a reason for hiding this comment

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

Instance field introduces unnecessary state to what should be a utility class. This violates the principle of keeping utility classes stateless and thread-safe.


private PreferencesMigrations() {
private PreferencesMigrations(JabRefGuiPreferences preferences) {
this.preferences = preferences;
}

/**
Expand Down Expand Up @@ -562,7 +557,21 @@ static void moveApiKeysToKeyring(JabRefCliPreferences preferences) {
* The tab "Comments" is hard coded using {@link CommentsTab} since v5.10 (and thus hard-wired in {@link org.jabref.gui.entryeditor.EntryEditor#createTabs()}.
* Thus, the configuration ih the preferences is obsolete
*/

Copy link
Member

Choose a reason for hiding this comment

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

Some AI tool always adds an empty line above the method - which AI tool is this?

This is wrong!

static void removeCommentsFromCustomEditorTabs(GuiPreferences preferences) {
preferences.getEntryEditorPreferences().getEntryEditorTabs().remove("Comments");
}

public void addICORERankingFieldToGeneralTab() {
String key = "entryEditorTabList";
String expectedValue = "General:doi;crossref;keywords;eprint;url;file;groups;owner;timestamp;printed;priority;qualityassured;ranking;readstatus;relevance";
String newValue = "General:doi;icoreranking;crossref;keywords;eprint;url;file;groups;owner;timestamp;printed;priority;qualityassured;ranking;readstatus;relevance";

String oldValue = preferences.get(key);

if (expectedValue.equals(oldValue)) {
preferences.put(key, newValue);
}
}

}
1 change: 1 addition & 0 deletions jablib/src/main/java/module-info.java
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
open module org.jabref.jablib {
exports org.jabref.logic.icore;
exports org.jabref.model;
exports org.jabref.logic;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package org.jabref.logic.icore;

public record ConferenceRankingEntry(
Copy link

Choose a reason for hiding this comment

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

The record lacks input validation for required fields. Some fields like title or acronym should not be null. Consider adding compact constructor with validation.

String title,
String acronym,
String source,
String rank,
String note,
String dblp,
String primaryFor,
String averageRating
) {
@Override
public String toString() {
return String.format("""
Title: %s
Acronym: %s
Source: %s
Rank: %s
Note: %s
DBLP: %s
Primary FoR: %s
Average Rating: %s
""", title, acronym, source, rank, note, dblp, primaryFor, averageRating);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package org.jabref.logic.icore;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

import org.jabref.logic.util.strings.StringSimilarity;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class ICoreRankingRepository {

final Map<String, String> acronymToRank = new HashMap<>();
private Map<String, String> nameToRank = new HashMap<>();
private Map<String, ConferenceRankingEntry> acronymMap = new HashMap<>();
private Map<String, ConferenceRankingEntry> nameMap = new HashMap<>();
Comment on lines +19 to +22
Copy link

Choose a reason for hiding this comment

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

Use modern Java data structures. HashMap initialization can be replaced with more concise Map.of() for empty maps, following modern Java practices.

private StringSimilarity similarity = new StringSimilarity();
private Logger LOGGER = LoggerFactory.getLogger(ICoreRankingRepository.class);

public ICoreRankingRepository() {
InputStream inputStream = getClass().getResourceAsStream("/ICORE.csv");
if (inputStream == null) {
LOGGER.error("ICORE.csv not found in resources.");
return;
}

try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
reader.lines().skip(1).forEach(line -> {
String[] parts = line.split(",(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)", -1);
Copy link
Member

Choose a reason for hiding this comment

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

No.

There are libraries for this. In JabRef, we use https://commons.apache.org/proper/commons-csv/

if (parts.length >= 9) {
String name = parts[0].trim().toLowerCase();
String acronym = parts[1].trim().toLowerCase();
String rank = parts[3].trim();
acronymToRank.put(acronym, rank);
nameToRank.put(name, rank);
}
ConferenceRankingEntry entry = new ConferenceRankingEntry(
parts[0].trim(), // title
parts[1].trim(), // acronym
parts[2].trim(), // source
parts[3].trim(), // rank
parts[4].trim(), // note
parts[5].trim(), // dblp
parts[6].trim(), // primaryFor
parts[8].trim() // averageRating
);
acronymMap.put(entry.acronym().toLowerCase(), entry);
nameMap.put(entry.title().toLowerCase(), entry);
});

// System.out.println("Loaded entries:");
// acronymToRank.forEach((key, val) -> System.out.println("Acronym: " + key + " -> " + val));
// nameToRank.forEach((key, val) -> System.out.println("Name: " + key + " -> " + val));
} catch (NullPointerException | IOException e) {
Copy link

Choose a reason for hiding this comment

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

NullPointerException should not be caught explicitly as it indicates a programming error. Additionally, only logging debug for a critical data loading failure is insufficient.

LOGGER.debug("Failed to load ICORE ranking data {}", e.getMessage());
}
}

public Optional<String> getRankingFor(String acronymOrName) {
String key = acronymOrName.trim().toLowerCase();

if (acronymToRank.containsKey(key)) {
return Optional.of(acronymToRank.get(key));
}

if (nameToRank.containsKey(key)) {
return Optional.of(nameToRank.get(key));
}

if (key.length() < 6) {
LOGGER.debug("Skipped fuzzy fallback for short string: {}", key);
return Optional.empty();
}

LOGGER.debug("Fuzzy match fallback triggered for: {}", key);
return nameToRank.entrySet().stream()
.filter(e -> similarity.editDistanceIgnoreCase(e.getKey(), key) < 0.01)
.peek(e -> LOGGER.debug("Fuzzy match candidate: {}", e.getKey()))
.map(Map.Entry::getValue)
.findFirst();
}

public Optional<ConferenceRankingEntry> getFullEntry(String acronymOrName) {
String key = acronymOrName.trim().toLowerCase();
if (acronymMap.containsKey(key)) {
return Optional.of(acronymMap.get(key));
}
if (nameMap.containsKey(key)) {
return Optional.of(nameMap.get(key));
}
return Optional.empty();
}
}
15 changes: 15 additions & 0 deletions jablib/src/main/java/org/jabref/logic/util/ConferenceUtil.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package org.jabref.logic.util;

import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class ConferenceUtil {
public static Optional<String> extractAcronym(String title) {
Matcher matcher = Pattern.compile("\\((.*?)\\)").matcher(title);
Copy link
Member

Choose a reason for hiding this comment

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

Pattern needs to be a private static final variable to be really speed saving

if (matcher.find()) {
return Optional.of(matcher.group(1));
}
return Optional.empty();
}
}
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.ICORERANKING, StandardField.CROSSREF, StandardField.KEYWORDS, StandardField.EPRINT, StandardField.URL, StandardField.FILE, StandardField.GROUPS, StandardField.OWNER, StandardField.TIMESTAMP));
Copy link

Choose a reason for hiding this comment

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

Using ArrayList with Arrays.asList is an outdated pattern. Modern Java practices suggest using List.of() for creating immutable lists, which is more concise and efficient.

defaultGeneralFields.addAll(EnumSet.allOf(SpecialField.class));
return defaultGeneralFields;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,9 @@ public enum StandardField implements Field {
OWNER("owner"),
TIMESTAMP("timestamp", FieldProperty.DATE),
CREATIONDATE("creationdate", FieldProperty.DATE),
MODIFICATIONDATE("modificationdate", FieldProperty.DATE);

MODIFICATIONDATE("modificationdate", FieldProperty.DATE),
ICORERANKING("icoreranking");
Comment on lines +142 to +143
Copy link

Choose a reason for hiding this comment

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

Extra blank line added between fields without adding new statements, which violates code formatting consistency within the enum declaration.


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

Expand Down
Loading
Loading