Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
547d217
Improve INSPIRE citekey handling and cleanup
SLin417 Oct 7, 2025
67116d5
Merge pull request #1 from SLin417/sonia-inspirefetcher
u7978428 Oct 8, 2025
91ae89f
Provide a switch to let users choose whether to enable the texkeys fu…
u7978428 Oct 8, 2025
9c6d882
feat(3.2): Add retry mechanism and validation to INSPIRE fetcher
QiyuanHananu Oct 9, 2025
64b8d07
Merge main into qiyuan-inspireretry
QiyuanHananu Oct 9, 2025
f248c71
fix: Adjust timeout and exception handling for URLDownload compatibility
QiyuanHananu Oct 9, 2025
bc84296
Merge pull request #3 from SLin417/qiyuan-inspireretry
QiyuanHananu Oct 9, 2025
eb48ff1
Merge pull request #4 from SLin417/main
SLin417 Oct 10, 2025
6133c0b
Use CitationKeyGenerator to generate new keys for INSPIREFetcher and …
SLin417 Oct 10, 2025
bb0a220
Merge pull request #5 from SLin417/sonia-inspirefetcher
u7978428 Oct 10, 2025
4f2b006
Merge pull request #2 from SLin417/yuxiao-keycleanup
Junqi597 Oct 13, 2025
f6907cc
Implement identifier detection and UI prompt enhancement
u7978428 Oct 14, 2025
7da31e2
Merge pull request #6 from SLin417/yuxiao-keycleanup
Junqi597 Oct 14, 2025
c276938
feat: enhance INSPIREFetcher with texkeys extraction and retry mechan…
Junqi597 Oct 14, 2025
5de358d
feat: prioritize INSPIREFetcher for arXiv identifiers with fallback.
Junqi597 Oct 14, 2025
c773d99
Merge branch 'main' into junqi-compositeidfetcher
Junqi597 Oct 14, 2025
8ba0fad
Add long citation key detection with user guidance notification.
u7978428 Oct 15, 2025
6e7a5a6
Merge pull request #7 from SLin417/junqi-compositeidfetcher
SLin417 Oct 16, 2025
7f8d425
Merge pull request #8 from SLin417/yuxiao-keycleanup
Junqi597 Oct 16, 2025
0b6bd5d
Add logging to WebSearch ViewModels
u7978428 Oct 19, 2025
303975f
test
AmandaDec Oct 23, 2025
1bc4cba
test
AmandaDec Oct 23, 2025
4e82452
Merge pull request #9 from SLin417/yuxiao-keycleanup
Junqi597 Oct 25, 2025
f1b63d3
test
AmandaDec Oct 25, 2025
12defef
test
AmandaDec Oct 23, 2025
cdcb119
test
AmandaDec Oct 23, 2025
643d45c
Add routing and texkeys tests for INSPIRE fetcher and OpenAlex parser…
AmandaDec Oct 25, 2025
c9cb687
Merge origin/zhouran-routingtests: resolve conflicts and fix INSPIREF…
AmandaDec Oct 25, 2025
8a97466
test
AmandaDec Oct 25, 2025
3e23ec0
debug
AmandaDec Oct 25, 2025
717fbc6
Move doPostCleanup to performSearch after setTexkeys
SLin417 Oct 25, 2025
c343089
Rewrite INSPIREFetcherTexkeysTest
SLin417 Oct 25, 2025
9e5494a
Rewrite OpenAlexParserTest
SLin417 Oct 25, 2025
689fbaf
Correct the names of tests.
SLin417 Oct 25, 2025
98952c5
Correct the names of test classes.
SLin417 Oct 25, 2025
4220be8
changes for import static org.junit.jupiter.api.Assertions.*
AmandaDec Oct 26, 2025
de3509f
Fix compilation errors in test classes
QiyuanHananu Oct 26, 2025
e7cfd1c
delete test
AmandaDec Oct 26, 2025
6d92ffa
add test
AmandaDec Oct 26, 2025
2e69968
Fix bugs in WebSearchPaneViewModelIdentifierTest.
SLin417 Oct 26, 2025
ee5e5db
Merge pull request #11 from SLin417/zhouran-routingtests
SLin417 Oct 26, 2025
4cafceb
Merge branch 'main' into main
SLin417 Oct 26, 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 build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,4 @@ tasks.cyclonedxBom {
componentVersion = project.version.toString()
componentGroup = "org.jabref"
}

Copy link
Member

Choose a reason for hiding this comment

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

This is an indication that an AI was used without double check.

Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
import org.jabref.model.entry.BibtexString;
import org.jabref.model.entry.LinkedFile;
import org.jabref.model.entry.field.StandardField;
import org.jabref.model.entry.identifier.Identifier;
import org.jabref.model.groups.GroupEntryChanger;
import org.jabref.model.groups.GroupTreeNode;
import org.jabref.model.groups.SmartGroup;
Expand Down Expand Up @@ -230,6 +231,9 @@ public void importEntries(List<BibEntry> entries) {
ImportCleanup cleanup = ImportCleanup.targeting(bibDatabaseContext.getMode(), preferences.getFieldPreferences());
cleanup.doPostCleanup(entries);
importCleanedEntries(entries);

// Check for long keys and show guidance notification
checkForLongKeysAndNotify(entries);
}

public void importCleanedEntries(List<BibEntry> entries) {
Expand Down Expand Up @@ -526,4 +530,39 @@ private void addToImportEntriesGroup(List<BibEntry> entriesToInsert) {
.ifPresent(smtGrp -> smtGrp.addEntriesToGroup(entriesToInsert));
}
}

/**
* Checks for entries with long citation keys and shows a guidance notification if found.
* This helps users understand when they might want to use shorter, more manageable keys.
*/
private void checkForLongKeysAndNotify(List<BibEntry> entries) {
if (entries.isEmpty()) {
return;
}

// Define threshold for "long" keys (e.g., more than 20 characters)
// Temporarily lowered for testing/demo purposes
final int LONG_KEY_THRESHOLD = 20;

List<BibEntry> entriesWithLongKeys = entries.stream()
.filter(entry -> {
String citationKey = entry.getCitationKey().orElse("");
return citationKey.length() > LONG_KEY_THRESHOLD;
})
.toList();

if (!entriesWithLongKeys.isEmpty()) {
String message = Localization.lang("Imported %0 entries with long citation keys (>%1 characters). " +
"Consider using shorter keys for better readability and management.",
entriesWithLongKeys.size(), LONG_KEY_THRESHOLD);

LOGGER.info("Long key detection: Found {} entries with keys longer than {} characters",
entriesWithLongKeys.size(), LONG_KEY_THRESHOLD);

// Show notification to user
UiTaskExecutor.runInJavaFXThread(() ->
dialogService.notify(message)
);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import javafx.css.PseudoClass;
import javafx.scene.control.Button;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.control.Tooltip;
import javafx.scene.input.KeyCode;
Expand Down Expand Up @@ -52,6 +53,7 @@ private void initialize() {
getChildren().addAll(
fetcherContainer,
createQueryField(),
createIdentifierHint(),
createSearchButton()
);
this.disableProperty().bind(searchDisabledProperty());
Expand Down Expand Up @@ -108,6 +110,34 @@ private TextField createQueryField() {
return query;
}

/**
* Create identifier hint label
*/
private Label createIdentifierHint() {
Label identifierHint = new Label();
identifierHint.getStyleClass().add("identifier-hint");
identifierHint.visibleProperty().bind(viewModel.identifierDetectedProperty());

// Use EasyBind to create dynamic text binding
EasyBind.subscribe(viewModel.identifierDetectedProperty(), detected -> {
if (detected) {
String identifierType = viewModel.getDetectedIdentifierType();
identifierHint.setText(Localization.lang("Detected identifier: %0", identifierType));
} else {
identifierHint.setText("");
}
});

// Also listen to identifier type changes
EasyBind.subscribe(viewModel.detectedIdentifierTypeProperty(), identifierType -> {
if (viewModel.isIdentifierDetected()) {
identifierHint.setText(Localization.lang("Detected identifier: %0", identifierType));
}
});

return identifierHint;
}

/**
* Create button that triggers search
*/
Expand Down Expand Up @@ -147,4 +177,4 @@ private ObservableBooleanValue searchDisabledProperty() {
stateManager.getOpenDatabases()
);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package org.jabref.gui.importer.fetcher;

import java.util.Optional;
import java.util.concurrent.Callable;

import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ListProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleListProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
Expand All @@ -22,6 +25,7 @@
import org.jabref.logic.importer.WebFetchers;
import org.jabref.logic.l10n.Localization;
import org.jabref.logic.util.BackgroundTask;
import org.jabref.model.entry.identifier.Identifier;
import org.jabref.model.search.query.SearchQuery;
import org.jabref.model.strings.StringUtil;
import org.jabref.model.util.OptionalUtil;
Expand All @@ -34,12 +38,18 @@
import org.antlr.v4.runtime.RecognitionException;
import org.antlr.v4.runtime.Token;
import org.antlr.v4.runtime.misc.ParseCancellationException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class WebSearchPaneViewModel {

private static final Logger LOGGER = LoggerFactory.getLogger(WebSearchPaneViewModel.class);

private final ObjectProperty<SearchBasedFetcher> selectedFetcher = new SimpleObjectProperty<>();
private final ListProperty<SearchBasedFetcher> fetchers = new SimpleListProperty<>(FXCollections.observableArrayList());
private final StringProperty query = new SimpleStringProperty();
private final BooleanProperty identifierDetected = new SimpleBooleanProperty(false);
private final StringProperty detectedIdentifierType = new SimpleStringProperty("");
private final DialogService dialogService;
private final GuiPreferences preferences;
private final StateManager stateManager;
Expand Down Expand Up @@ -68,6 +78,9 @@ public WebSearchPaneViewModel(GuiPreferences preferences, DialogService dialogSe
sidePanePreferences.setWebSearchFetcherSelected(newIndex);
});

// Subscribe to query changes to detect identifiers
EasyBind.subscribe(query, this::updateIdentifierDetection);

searchQueryValidator = new FunctionBasedValidator<>(
query,
queryText -> {
Expand All @@ -94,7 +107,7 @@ public WebSearchPaneViewModel(GuiPreferences preferences, DialogService dialogSe
int line = offendingToken.getLine();
int charPositionInLine = offendingToken.getCharPositionInLine() + 1;

return ValidationMessage.error(Localization.lang("Invalid query element '%0' at position %1", line, charPositionInLine));
return ValidationMessage.error(Localization.lang("Invalid query element '%0' at position %1", offendingToken.getText(), charPositionInLine));
}

// Fallback for other failing reasons
Expand Down Expand Up @@ -128,48 +141,116 @@ public StringProperty queryProperty() {
}

public void search() {
String query = getQuery().trim();
LOGGER.debug("Starting web search with query: '{}'", query);

if (!preferences.getImporterPreferences().areImporterEnabled()) {
if (!preferences.getImporterPreferences().areImporterEnabled()) {
dialogService.notify(Localization.lang("Web search disabled"));
return;
}
LOGGER.warn("Web search attempted but importers are disabled");
dialogService.notify(Localization.lang("Web search disabled"));
return;
}

String query = getQuery().trim();
if (StringUtil.isBlank(query)) {
LOGGER.warn("Web search attempted with empty query");
dialogService.notify(Localization.lang("Please enter a search string"));
return;
}
if (stateManager.getActiveDatabase().isEmpty()) {
LOGGER.warn("Web search attempted but no database is open");
dialogService.notify(Localization.lang("Please open or start a new library before searching"));
return;
}

SearchBasedFetcher activeFetcher = getSelectedFetcher();
LOGGER.info("Performing web search using fetcher: {} with query: '{}'",
activeFetcher.getName(), query);

Callable<ParserResult> parserResultCallable;

String fetcherName = activeFetcher.getName();

if (CompositeIdFetcher.containsValidId(query)) {
LOGGER.debug("Query contains valid identifier, using CompositeIdFetcher");
CompositeIdFetcher compositeIdFetcher = new CompositeIdFetcher(preferences.getImportFormatPreferences());
parserResultCallable = () -> new ParserResult(OptionalUtil.toList(compositeIdFetcher.performSearchById(query)));
fetcherName = Localization.lang("Identifier-based Web Search");
} else {
LOGGER.debug("Query is a regular search query, using selected fetcher");
// Exceptions are handled below at "task.onFailure(dialogService::showErrorDialogAndWait)"
parserResultCallable = () -> new ParserResult(activeFetcher.performSearch(query));
}

BackgroundTask<ParserResult> task = BackgroundTask.wrap(parserResultCallable)
.withInitialMessage(Localization.lang("Processing \"%0\"...", query));
task.onFailure(dialogService::showErrorDialogAndWait);
task.onFailure(exception -> {
LOGGER.error("Web search failed for query '{}' using fetcher '{}': {}",
query, activeFetcher.getName(), exception.getMessage());
dialogService.showErrorDialogAndWait(exception);
});

ImportEntriesDialog dialog = new ImportEntriesDialog(stateManager.getActiveDatabase().get(), task, activeFetcher, query);
dialog.setTitle(fetcherName);
dialogService.showCustomDialogAndWait(dialog);

LOGGER.debug("Web search dialog completed for query: '{}'", query);
}

public ValidationStatus queryValidationStatus() {
return searchQueryValidator.getValidationStatus();
}
}

public boolean isIdentifierDetected() {
return identifierDetected.get();
}

public BooleanProperty identifierDetectedProperty() {
return identifierDetected;
}

public String getDetectedIdentifierType() {
return detectedIdentifierType.get();
}

public StringProperty detectedIdentifierTypeProperty() {
return detectedIdentifierType;
}

private void updateIdentifierDetection(String queryText) {
if (StringUtil.isBlank(queryText)) {
identifierDetected.set(false);
detectedIdentifierType.set("");
LOGGER.debug("Identifier detection cleared for empty query");
return;
}

Optional<Identifier> identifier = Identifier.from(queryText.trim());
if (identifier.isPresent()) {
String identifierType = getIdentifierTypeName(identifier.get());
identifierDetected.set(true);
detectedIdentifierType.set(identifierType);
LOGGER.debug("Identifier detected: {} for query: '{}'", identifierType, queryText);
} else {
identifierDetected.set(false);
detectedIdentifierType.set("");
LOGGER.debug("No identifier detected for query: '{}'", queryText);
}
}

private String getIdentifierTypeName(Identifier identifier) {
String className = identifier.getClass().getSimpleName();
switch (className) {
case "DOI":
return Localization.lang("DOI");
case "ArXivIdentifier":
return Localization.lang("ArXiv");
case "ISBN":
return Localization.lang("ISBN");
case "SSRN":
return Localization.lang("SSRN");
case "RFC":
return Localization.lang("RFC");
default:
return className;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,16 @@
import org.jabref.gui.undo.NamedCompoundEdit;
import org.jabref.gui.undo.UndoableChangeType;
import org.jabref.gui.undo.UndoableFieldChange;
import org.jabref.logic.citationkeypattern.CitationKeyGenerator;
import org.jabref.logic.importer.EntryBasedFetcher;
import org.jabref.logic.importer.FetcherClientException;
import org.jabref.logic.importer.FetcherServerException;
import org.jabref.logic.importer.IdBasedFetcher;
import org.jabref.logic.importer.ImportCleanup;
import org.jabref.logic.importer.WebFetcher;
import org.jabref.logic.importer.WebFetchers;
import org.jabref.logic.importer.fetcher.ArXivFetcher;
import org.jabref.logic.importer.fetcher.INSPIREFetcher;
import org.jabref.logic.l10n.Localization;
import org.jabref.logic.util.BackgroundTask;
import org.jabref.logic.util.TaskExecutor;
Expand Down Expand Up @@ -172,6 +175,14 @@ public void fetchAndMerge(BibEntry entry, EntryBasedFetcher fetcher) {
if (fetchedEntry.isPresent()) {
ImportCleanup cleanup = ImportCleanup.targeting(bibDatabaseContext.getMode(), preferences.getFieldPreferences());
cleanup.doPostCleanup(fetchedEntry.get());

// Generate a new citation key for entries fetched by INSPIREFetcher or ArXivFetcher
// to avoid long key
if (fetcher instanceof INSPIREFetcher || fetcher instanceof ArXivFetcher) {
CitationKeyGenerator keyGen = new CitationKeyGenerator(bibDatabaseContext, preferences.getCitationKeyPatternPreferences());
keyGen.generateAndSetKey(fetchedEntry.get());
}

showMergeDialog(entry, fetchedEntry.get(), fetcher);
} else {
dialogService.notify(Localization.lang("Could not find any bibliographic information."));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ public class WebSearchTab extends AbstractPreferenceTabView<WebSearchTabViewMode
@FXML private CheckBox grobidEnabled;
@FXML private TextField grobidURL;

@FXML private CheckBox preferInspireTexkeys;

@FXML private VBox fetchersContainer;

private final ReadOnlyBooleanProperty refAiEnabled;
Expand Down Expand Up @@ -119,6 +121,8 @@ public void initialize() {
useCustomDOIName.textProperty().bindBidirectional(viewModel.useCustomDOINameProperty());
useCustomDOIName.disableProperty().bind(useCustomDOI.selectedProperty().not());

preferInspireTexkeys.selectedProperty().bindBidirectional(viewModel.preferInspireTexkeysProperty());

InvalidationListener listener = _ -> fetchersContainer
.getChildren()
.setAll(viewModel.getFetchers()
Expand Down
Loading
Loading