-
-
Notifications
You must be signed in to change notification settings - Fork 2.9k
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
base: main
Are you sure you want to change the base?
Add ICORE Ranking support #13512
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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
|
||
|
||
## [6.0-alpha2] – 2025-04-27 | ||
|
||
### Added | ||
|
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"); | ||
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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. also |
||
} | ||
|
||
Optional<String> finalBookTitle = bookTitle; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.*; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
|
@@ -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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
} | ||
|
||
/** | ||
|
@@ -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 | ||
*/ | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
} | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
package org.jabref.logic.icore; | ||
|
||
public record ConferenceRankingEntry( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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(); | ||
} | ||
} |
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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
if (matcher.find()) { | ||
return Optional.of(matcher.group(1)); | ||
} | ||
return Optional.empty(); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
|
||
|
There was a problem hiding this comment.
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.