diff --git a/build.gradle.kts b/build.gradle.kts index 2d14d6973d1..cee2d416a94 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -66,3 +66,4 @@ tasks.cyclonedxBom { componentVersion = project.version.toString() componentGroup = "org.jabref" } + diff --git a/jabgui/src/main/java/org/jabref/gui/externalfiles/ImportHandler.java b/jabgui/src/main/java/org/jabref/gui/externalfiles/ImportHandler.java index 8d43ed1ac51..1d823f294f8 100644 --- a/jabgui/src/main/java/org/jabref/gui/externalfiles/ImportHandler.java +++ b/jabgui/src/main/java/org/jabref/gui/externalfiles/ImportHandler.java @@ -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; @@ -230,6 +231,9 @@ public void importEntries(List 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 entries) { @@ -526,4 +530,39 @@ private void addToImportEntriesGroup(List 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 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 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) + ); + } + } } diff --git a/jabgui/src/main/java/org/jabref/gui/importer/fetcher/WebSearchPaneView.java b/jabgui/src/main/java/org/jabref/gui/importer/fetcher/WebSearchPaneView.java index d5347ccc061..192d38b94b9 100644 --- a/jabgui/src/main/java/org/jabref/gui/importer/fetcher/WebSearchPaneView.java +++ b/jabgui/src/main/java/org/jabref/gui/importer/fetcher/WebSearchPaneView.java @@ -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; @@ -52,6 +53,7 @@ private void initialize() { getChildren().addAll( fetcherContainer, createQueryField(), + createIdentifierHint(), createSearchButton() ); this.disableProperty().bind(searchDisabledProperty()); @@ -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 */ @@ -147,4 +177,4 @@ private ObservableBooleanValue searchDisabledProperty() { stateManager.getOpenDatabases() ); } -} +} \ No newline at end of file diff --git a/jabgui/src/main/java/org/jabref/gui/importer/fetcher/WebSearchPaneViewModel.java b/jabgui/src/main/java/org/jabref/gui/importer/fetcher/WebSearchPaneViewModel.java index 118a0d6a6dd..d3924066e8a 100644 --- a/jabgui/src/main/java/org/jabref/gui/importer/fetcher/WebSearchPaneViewModel.java +++ b/jabgui/src/main/java/org/jabref/gui/importer/fetcher/WebSearchPaneViewModel.java @@ -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; @@ -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; @@ -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 selectedFetcher = new SimpleObjectProperty<>(); private final ListProperty 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; @@ -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 -> { @@ -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 @@ -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 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 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.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; + } + } +} \ No newline at end of file diff --git a/jabgui/src/main/java/org/jabref/gui/mergeentries/FetchAndMergeEntry.java b/jabgui/src/main/java/org/jabref/gui/mergeentries/FetchAndMergeEntry.java index 63666e0e464..8e41b8d5411 100644 --- a/jabgui/src/main/java/org/jabref/gui/mergeentries/FetchAndMergeEntry.java +++ b/jabgui/src/main/java/org/jabref/gui/mergeentries/FetchAndMergeEntry.java @@ -16,6 +16,7 @@ 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; @@ -23,6 +24,8 @@ 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; @@ -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.")); diff --git a/jabgui/src/main/java/org/jabref/gui/preferences/websearch/WebSearchTab.java b/jabgui/src/main/java/org/jabref/gui/preferences/websearch/WebSearchTab.java index 12064d26e92..ff3ae253b6a 100644 --- a/jabgui/src/main/java/org/jabref/gui/preferences/websearch/WebSearchTab.java +++ b/jabgui/src/main/java/org/jabref/gui/preferences/websearch/WebSearchTab.java @@ -45,6 +45,8 @@ public class WebSearchTab extends AbstractPreferenceTabView fetchersContainer .getChildren() .setAll(viewModel.getFetchers() diff --git a/jabgui/src/main/java/org/jabref/gui/preferences/websearch/WebSearchTabViewModel.java b/jabgui/src/main/java/org/jabref/gui/preferences/websearch/WebSearchTabViewModel.java index dca966ff906..3e707357142 100644 --- a/jabgui/src/main/java/org/jabref/gui/preferences/websearch/WebSearchTabViewModel.java +++ b/jabgui/src/main/java/org/jabref/gui/preferences/websearch/WebSearchTabViewModel.java @@ -46,8 +46,12 @@ import org.jabref.logic.util.TaskExecutor; import kong.unirest.core.UnirestException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class WebSearchTabViewModel implements PreferenceTabViewModel { + + private static final Logger LOGGER = LoggerFactory.getLogger(WebSearchTabViewModel.class); private final BooleanProperty enableWebSearchProperty = new SimpleBooleanProperty(); private final BooleanProperty warnAboutDuplicatesOnImportProperty = new SimpleBooleanProperty(); private final BooleanProperty shouldDownloadLinkedOnlineFiles = new SimpleBooleanProperty(); @@ -69,6 +73,8 @@ public class WebSearchTabViewModel implements PreferenceTabViewModel { private final BooleanProperty grobidEnabledProperty = new SimpleBooleanProperty(); private final StringProperty grobidURLProperty = new SimpleStringProperty(""); + private final BooleanProperty preferInspireTexkeysProperty = new SimpleBooleanProperty(); + private final BooleanProperty apikeyPersistProperty = new SimpleBooleanProperty(); private final BooleanProperty apikeyPersistAvailableProperty = new SimpleBooleanProperty(); @@ -138,6 +144,8 @@ private void setupPlainCitationParsers(CliPreferences preferences) { @Override public void setValues() { + LOGGER.debug("Setting values for WebSearchTabViewModel"); + enableWebSearchProperty.setValue(importerPreferences.areImporterEnabled()); warnAboutDuplicatesOnImportProperty.setValue(importerPreferences.shouldWarnAboutDuplicatesOnImport()); shouldDownloadLinkedOnlineFiles.setValue(filePreferences.shouldDownloadLinkedFiles()); @@ -146,6 +154,11 @@ public void setValues() { addImportedEntriesGroupName.setValue(libraryPreferences.getAddImportedEntriesGroupName()); defaultPlainCitationParser.setValue(importerPreferences.getDefaultPlainCitationParser()); citationsRelationStoreTTL.setValue(importerPreferences.getCitationsRelationsStoreTTL()); + + LOGGER.debug("Web search enabled: {}, Duplicate warning: {}, Download linked files: {}", + importerPreferences.areImporterEnabled(), + importerPreferences.shouldWarnAboutDuplicatesOnImport(), + filePreferences.shouldDownloadLinkedFiles()); useCustomDOIProperty.setValue(doiPreferences.isUseCustom()); useCustomDOINameProperty.setValue(doiPreferences.getDefaultBaseURI()); @@ -153,6 +166,8 @@ public void setValues() { grobidEnabledProperty.setValue(grobidPreferences.isGrobidEnabled()); grobidURLProperty.setValue(grobidPreferences.getGrobidURL()); + preferInspireTexkeysProperty.setValue(preferences.getImporterPreferences().isPreferInspireTexkeys()); + Set savedApiKeys = preferences.getImporterPreferences().getApiKeys(); Set enabledCatalogs = new HashSet<>(importerPreferences.getCatalogs()); @@ -188,11 +203,18 @@ public void setValues() { @Override public void storeSettings() { + LOGGER.debug("Storing WebSearchTabViewModel settings"); + importerPreferences.setImporterEnabled(enableWebSearchProperty.getValue()); importerPreferences.setWarnAboutDuplicatesOnImport(warnAboutDuplicatesOnImportProperty.getValue()); filePreferences.setDownloadLinkedFiles(shouldDownloadLinkedOnlineFiles.getValue()); filePreferences.setKeepDownloadUrl(shouldkeepDownloadUrl.getValue()); libraryPreferences.setAddImportedEntries(addImportedEntries.getValue()); + + LOGGER.info("Web search settings updated - Enabled: {}, Duplicate warning: {}, Download linked files: {}", + enableWebSearchProperty.getValue(), + warnAboutDuplicatesOnImportProperty.getValue(), + shouldDownloadLinkedOnlineFiles.getValue()); if (addImportedEntriesGroupName.getValue().isEmpty() || addImportedEntriesGroupName.getValue().startsWith(" ")) { libraryPreferences.setAddImportedEntriesGroupName(Localization.lang("Imported entries")); } else { @@ -207,6 +229,8 @@ public void storeSettings() { doiPreferences.setUseCustom(useCustomDOIProperty.get()); doiPreferences.setDefaultBaseURI(useCustomDOINameProperty.getValue().trim()); + importerPreferences.setPreferInspireTexkeys(preferInspireTexkeysProperty.getValue()); + importerPreferences.setCatalogs( fetchers.stream() .filter(FetcherViewModel::isEnabled) @@ -289,20 +313,29 @@ public IntegerProperty citationsRelationsStoreTTLProperty() { return citationsRelationStoreTTL; } + public BooleanProperty preferInspireTexkeysProperty() { + return preferInspireTexkeysProperty; + } + public void checkApiKey(FetcherViewModel fetcherViewModel, String apiKey, Consumer onFinished) { + LOGGER.debug("Checking API key for fetcher: {}", fetcherViewModel.getName()); + Callable tester = () -> { WebFetcher webFetcher = fetcherViewModel.getFetcher(); if (!(webFetcher instanceof CustomizableKeyFetcher fetcher)) { + LOGGER.warn("Fetcher {} is not a CustomizableKeyFetcher", fetcherViewModel.getName()); return false; } String testUrlWithoutApiKey = fetcher.getTestUrl(); if (testUrlWithoutApiKey == null) { + LOGGER.warn("No test URL available for fetcher: {}", fetcherViewModel.getName()); return false; } if (apiKey.isEmpty()) { + LOGGER.warn("Empty API key provided for fetcher: {}", fetcherViewModel.getName()); return false; } @@ -310,14 +343,24 @@ public void checkApiKey(FetcherViewModel fetcherViewModel, String apiKey, Consum URLDownload urlDownload = new URLDownload(testUrlWithoutApiKey + apiKey); // The HEAD request cannot be used because its response is not 200 (maybe 404 or 596...). int statusCode = ((HttpURLConnection) urlDownload.getSource().openConnection()).getResponseCode(); - return (statusCode >= 200) && (statusCode < 300); + boolean isValid = (statusCode >= 200) && (statusCode < 300); + LOGGER.debug("API key validation for {} returned status code: {}, valid: {}", + fetcherViewModel.getName(), statusCode, isValid); + return isValid; } catch (IOException | UnirestException e) { + LOGGER.warn("Error validating API key for fetcher {}: {}", fetcherViewModel.getName(), e.getMessage()); return false; } }; BackgroundTask.wrap(tester) - .onSuccess(onFinished) - .onFailure(_ -> onFinished.accept(false)) + .onSuccess(result -> { + LOGGER.info("API key validation completed for {}: {}", fetcherViewModel.getName(), result); + onFinished.accept(result); + }) + .onFailure(exception -> { + LOGGER.error("API key validation failed for {}: {}", fetcherViewModel.getName(), exception.getMessage()); + onFinished.accept(false); + }) .executeWith(taskExecutor); } diff --git a/jabgui/src/main/resources/images/Icons.properties b/jabgui/src/main/resources/images/Icons.properties index 8caa86aa6ad..2aaa10d313d 100644 --- a/jabgui/src/main/resources/images/Icons.properties +++ b/jabgui/src/main/resources/images/Icons.properties @@ -6,3 +6,5 @@ jabrefIcon40=JabRef-icon-40.png jabrefIcon48=JabRef-icon-48.png jabrefIcon64=JabRef-icon-64.png jabrefIcon128=JabRef-icon-128.png +INSPIRE\ preferences=Create property +Prefer\ INSPIRE\ texkeys;\ clean\ URL-like\ citation\ keys=Create property diff --git a/jabgui/src/main/resources/org/jabref/gui/preferences/websearch/WebSearchTab.fxml b/jabgui/src/main/resources/org/jabref/gui/preferences/websearch/WebSearchTab.fxml index cf9d45183de..db25d349073 100644 --- a/jabgui/src/main/resources/org/jabref/gui/preferences/websearch/WebSearchTab.fxml +++ b/jabgui/src/main/resources/org/jabref/gui/preferences/websearch/WebSearchTab.fxml @@ -47,6 +47,11 @@ +