-
-
Notifications
You must be signed in to change notification settings - Fork 2.9k
Add support for automatic ICORE conference ranking lookup [#13476] #13699
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?
Changes from all commits
d578f69
022bcd4
5bacb29
e035e0e
3a36afe
a7b1b94
33ed0db
806309e
5330632
b092aa9
52d3acd
75d2b47
45a1d10
290cf6c
93da09f
cbd4dda
2a1a73e
6747ad1
ff5c144
89c6600
c63e2da
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 |
---|---|---|
@@ -0,0 +1,93 @@ | ||
package org.jabref.gui.fieldeditors; | ||
|
||
import java.util.Optional; | ||
|
||
import javax.swing.undo.UndoManager; | ||
|
||
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.autocompleter.SuggestionProvider; | ||
import org.jabref.gui.preferences.GuiPreferences; | ||
import org.jabref.logic.icore.ConferenceRepository; | ||
import org.jabref.logic.integrity.FieldCheckers; | ||
import org.jabref.logic.l10n.Localization; | ||
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 ICORERankingEditor extends HBox implements FieldEditorFX { | ||
@FXML private ICORERankingEditorViewModel viewModel; | ||
@FXML private EditorTextField textField; | ||
@FXML private Button lookupICORERankButton; | ||
@FXML private Button visitICOREConferencePageButton; | ||
|
||
@Inject private DialogService dialogService; | ||
@Inject private UndoManager undoManager; | ||
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. Unused variable- remove 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'll remove the |
||
@Inject private GuiPreferences preferences; | ||
@Inject private ConferenceRepository conferenceRepository; | ||
|
||
private Optional<BibEntry> entry = Optional.empty(); | ||
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 Optional as a field is against Java best practices. Optional should only be used as method return type, not as a field or parameter type. |
||
|
||
public ICORERankingEditor(Field field, | ||
SuggestionProvider<?> suggestionProvider, | ||
FieldCheckers fieldCheckers) { | ||
|
||
Injector.registerExistingAndInject(this); | ||
|
||
ViewLoader.view(this) | ||
.root(this) | ||
.load(); | ||
|
||
this.viewModel = new ICORERankingEditorViewModel( | ||
field, | ||
suggestionProvider, | ||
fieldCheckers, | ||
dialogService, | ||
undoManager, | ||
preferences, | ||
conferenceRepository | ||
); | ||
|
||
textField.textProperty().bindBidirectional(viewModel.textProperty()); | ||
|
||
lookupICORERankButton.setTooltip( | ||
new Tooltip(Localization.lang("Look up conference rank")) | ||
); | ||
visitICOREConferencePageButton.setTooltip( | ||
new Tooltip(Localization.lang("Visit ICORE conference page")) | ||
); | ||
visitICOREConferencePageButton.disableProperty().bind(textField.textProperty().isEmpty()); | ||
|
||
new EditorValidator(preferences).configureValidation(viewModel.getFieldValidator().getValidationStatus(), textField); | ||
} | ||
|
||
@Override | ||
public void bindToEntry(BibEntry entry) { | ||
this.entry = Optional.of(entry); | ||
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. Direct assignment of Optional.of() could throw NullPointerException if entry is null. Should use Optional.ofNullable() for safer null handling. |
||
viewModel.bindToEntry(entry); | ||
} | ||
|
||
@Override | ||
public Parent getNode() { | ||
return this; | ||
} | ||
|
||
@FXML | ||
private void lookupRank() { | ||
entry.ifPresent(viewModel::lookupIdentifier); | ||
} | ||
|
||
@FXML | ||
private void openExternalLink() { | ||
viewModel.openExternalLink(); | ||
} | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
package org.jabref.gui.fieldeditors; | ||
|
||
import java.io.IOException; | ||
import java.util.Optional; | ||
|
||
import javax.swing.undo.UndoManager; | ||
|
||
import org.jabref.gui.DialogService; | ||
import org.jabref.gui.autocompleter.SuggestionProvider; | ||
import org.jabref.gui.desktop.os.NativeDesktop; | ||
import org.jabref.gui.preferences.GuiPreferences; | ||
import org.jabref.logic.icore.ConferenceAcronymExtractor; | ||
import org.jabref.logic.icore.ConferenceRepository; | ||
import org.jabref.logic.integrity.FieldCheckers; | ||
import org.jabref.logic.l10n.Localization; | ||
import org.jabref.model.entry.BibEntry; | ||
import org.jabref.model.entry.field.Field; | ||
import org.jabref.model.entry.field.StandardField; | ||
import org.jabref.model.icore.ConferenceEntry; | ||
|
||
import org.slf4j.Logger; | ||
import org.slf4j.LoggerFactory; | ||
|
||
public class ICORERankingEditorViewModel extends AbstractEditorViewModel { | ||
private static final Logger LOGGER = LoggerFactory.getLogger(ICORERankingEditorViewModel.class); | ||
|
||
private final DialogService dialogService; | ||
private final GuiPreferences preferences; | ||
private final ConferenceRepository repo; | ||
|
||
private ConferenceEntry matchedConference; | ||
|
||
public ICORERankingEditorViewModel( | ||
Field field, | ||
SuggestionProvider<?> suggestionProvider, | ||
FieldCheckers fieldCheckers, | ||
DialogService dialogService, | ||
UndoManager undoManager, | ||
GuiPreferences preferences, | ||
ConferenceRepository conferenceRepository | ||
) { | ||
super(field, suggestionProvider, fieldCheckers, undoManager); | ||
this.dialogService = dialogService; | ||
this.preferences = preferences; | ||
this.repo = conferenceRepository; | ||
} | ||
|
||
public void lookupIdentifier(BibEntry bibEntry) { | ||
Optional<String> bookTitle = bibEntry.getFieldOrAlias(StandardField.BOOKTITLE); | ||
|
||
if (bookTitle.isEmpty()) { | ||
bookTitle = bibEntry.getFieldOrAlias(StandardField.JOURNAL); | ||
} | ||
|
||
if (bookTitle.isEmpty()) { | ||
return; | ||
} | ||
|
||
Optional<ConferenceEntry> conference; | ||
Optional<String> acronym = ConferenceAcronymExtractor.extract(bookTitle.get()); | ||
if (acronym.isPresent()) { | ||
conference = repo.getConferenceFromAcronym(acronym.get()); | ||
if (conference.isPresent()) { | ||
entry.setField(field, conference.get().rank()); | ||
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 setField instead of withField violates JabRef's immutability pattern for BibEntry modifications. Should use withField to maintain immutability. |
||
matchedConference = conference.get(); | ||
return; | ||
} | ||
} | ||
|
||
conference = repo.getConferenceFromBookTitle(bookTitle.get()); | ||
if (conference.isPresent()) { | ||
entry.setField(field, conference.get().rank()); | ||
matchedConference = conference.get(); | ||
} else { | ||
dialogService.notify(Localization.lang("not found")); | ||
} | ||
} | ||
|
||
public void openExternalLink() { | ||
if (matchedConference != null) { | ||
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 null check instead of Optional for matchedConference field. New public methods and fields should use Optional to avoid null checks. |
||
try { | ||
NativeDesktop.openBrowser(matchedConference.getICOREURL(), preferences.getExternalApplicationsPreferences()); | ||
} catch (IOException e) { | ||
LOGGER.error("Error opening external link in browser", e); | ||
dialogService.showErrorDialogAndWait(Localization.lang("Could not open website."), e); | ||
} | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,6 +6,7 @@ | |
import java.util.List; | ||
import java.util.Map; | ||
import java.util.Optional; | ||
import java.util.Set; | ||
import java.util.function.UnaryOperator; | ||
import java.util.prefs.BackingStoreException; | ||
import java.util.prefs.Preferences; | ||
|
@@ -21,10 +22,13 @@ | |
import org.jabref.logic.citationkeypattern.GlobalCitationKeyPatterns; | ||
import org.jabref.logic.cleanup.CleanupPreferences; | ||
import org.jabref.logic.cleanup.FieldFormatterCleanups; | ||
import org.jabref.logic.l10n.Localization; | ||
import org.jabref.logic.os.OS; | ||
import org.jabref.logic.preferences.JabRefCliPreferences; | ||
import org.jabref.logic.shared.security.Password; | ||
import org.jabref.model.entry.BibEntryTypesManager; | ||
import org.jabref.model.entry.field.Field; | ||
import org.jabref.model.entry.field.FieldFactory; | ||
import org.jabref.model.entry.field.SpecialField; | ||
import org.jabref.model.entry.field.StandardField; | ||
import org.jabref.model.entry.types.EntryTypeFactory; | ||
|
@@ -65,6 +69,7 @@ public static void runMigrations(JabRefGuiPreferences preferences) { | |
upgradeCleanups(preferences); | ||
moveApiKeysToKeyring(preferences); | ||
removeCommentsFromCustomEditorTabs(preferences); | ||
addICORERankingFieldToGeneralTab(preferences); | ||
upgradeResolveBibTeXStringsFields(preferences); | ||
} | ||
|
||
|
@@ -558,6 +563,40 @@ static void moveApiKeysToKeyring(JabRefCliPreferences preferences) { | |
} | ||
} | ||
|
||
/** | ||
* Updates the default preferences for the editor fields under the "General" tab to include the ICORE Ranking Field | ||
* if it is missing. | ||
*<p> | ||
* The function first ensures that the current preferences match the previous default (before the ICORE field was added) | ||
* and only then does the update. | ||
* | ||
* @implNote The default fields for the "General" tab are defined by {@link FieldFactory#getDefaultGeneralFields()}. | ||
* @param preferences the user's current preferences | ||
*/ | ||
static void addICORERankingFieldToGeneralTab(GuiPreferences preferences) { | ||
koppor marked this conversation as resolved.
Show resolved
Hide resolved
|
||
Map<String, Set<Field>> entryEditorPrefs = preferences.getEntryEditorPreferences().getEntryEditorTabs(); | ||
Set<Field> currentGeneralPrefs = entryEditorPrefs.get(Localization.lang("General")); | ||
|
||
Set<Field> expectedGeneralPrefs = Set.of( | ||
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 |
||
StandardField.DOI, StandardField.CROSSREF, StandardField.KEYWORDS, StandardField.EPRINT, | ||
StandardField.URL, StandardField.FILE, StandardField.GROUPS, StandardField.OWNER, | ||
StandardField.TIMESTAMP, | ||
|
||
SpecialField.PRINTED, SpecialField.PRIORITY, SpecialField.QUALITY, SpecialField.RANKING, | ||
SpecialField.READ_STATUS, SpecialField.RELEVANCE | ||
); | ||
|
||
if (!currentGeneralPrefs.equals(expectedGeneralPrefs)) { | ||
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. Nice check! |
||
return; | ||
} | ||
|
||
entryEditorPrefs.put( | ||
Localization.lang("General"), | ||
FieldFactory.getDefaultGeneralFields().stream().collect(Collectors.toSet()) | ||
); | ||
preferences.getEntryEditorPreferences().setEntryEditorTabList(entryEditorPrefs); | ||
} | ||
|
||
/** | ||
* 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 | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
<?xml version="1.0" encoding="UTF-8"?> | ||
|
||
<?import javafx.scene.control.Button?> | ||
<?import javafx.scene.layout.HBox?> | ||
<?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.ICORERankingEditor"> | ||
<EditorTextField fx:id="textField"/> | ||
<Button fx:id="lookupICORERankButton" | ||
onAction="#lookupRank" | ||
styleClass="icon-button"> | ||
<graphic> | ||
<JabRefIconView glyph="LOOKUP_IDENTIFIER"/> | ||
</graphic> | ||
</Button> | ||
<Button fx:id="visitICOREConferencePageButton" | ||
onAction="#openExternalLink" | ||
styleClass="icon-button"> | ||
<graphic> | ||
<JabRefIconView glyph="OPEN_LINK"/> | ||
</graphic> | ||
</Button> | ||
</fx:root> |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -289,9 +289,10 @@ public static double correlateByWords(final String s1, final String s2) { | |
final String[] w1 = s1.split("\\s"); | ||
final String[] w2 = s2.split("\\s"); | ||
final int n = Math.min(w1.length, w2.length); | ||
final StringSimilarity match = new StringSimilarity(); | ||
int misses = 0; | ||
for (int i = 0; i < n; i++) { | ||
double corr = similarity(w1[i], w2[i]); | ||
double corr = match.similarity(w1[i], w2[i]); | ||
if (corr < 0.75) { | ||
misses++; | ||
} | ||
|
@@ -300,33 +301,6 @@ public static double correlateByWords(final String s1, final String s2) { | |
return 1 - missRate; | ||
} | ||
|
||
/** | ||
* Calculates the similarity (a number within 0 and 1) between two strings. | ||
* http://stackoverflow.com/questions/955110/similarity-string-comparison-in-java | ||
*/ | ||
private static double similarity(final String first, final String second) { | ||
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. Nice find :) |
||
final String longer; | ||
final String shorter; | ||
|
||
if (first.length() < second.length()) { | ||
longer = second; | ||
shorter = first; | ||
} else { | ||
longer = first; | ||
shorter = second; | ||
} | ||
|
||
final int longerLength = longer.length(); | ||
// both strings are zero length | ||
if (longerLength == 0) { | ||
return 1.0; | ||
} | ||
final double distanceIgnoredCase = new StringSimilarity().editDistanceIgnoreCase(longer, shorter); | ||
final double similarity = (longerLength - distanceIgnoredCase) / longerLength; | ||
LOGGER.trace("Longer string: {} Shorter string: {} Similarity: {}", longer, shorter, similarity); | ||
return similarity; | ||
} | ||
|
||
/** | ||
* Checks if the two entries represent the same publication. | ||
*/ | ||
|
Uh oh!
There was an error while loading. Please reload this page.