.
-```
-
-### Create a pull request
-
-Create a pull request on GitHub following GitHub's guide "[Creating a pull request from a fork](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request-from-a-fork)". For text inspirations, consider [How to write the perfect pull request](https://github.com/blog/1943-how-to-write-the-perfect-pull-request).
-
-If you want to indicate that a pull request is not yet complete **before** creating the pull request, you may consider creating a [draft pull request](https://github.blog/2019-02-14-introducing-draft-pull-requests/). Alternatively, once the PR has been created, you can add the prefix `[WIP]` (which stands for "Work in Progress") to indicate that the pull request is not yet complete, but you want to discuss something or inform about the current state of affairs.
-
-## How to improve the developer's documentation
-
-For improving developer's documentation, go on at the [docs/ subdirectory of JabRef's code](https://github.com/JabRef/jabref/tree/main/docs) and edit the file.
-GitHub offers a good guide at [Editing files in another user's repository](https://help.github.com/en/github/managing-files-in-a-repository/editing-files-in-another-users-repository).
-
-One can also add [callouts](https://just-the-docs.github.io/just-the-docs-tests/components/callouts/).
+Please head to our [contributing guide in the main repository](https://github.com/JabRef/jabref/blob/main/CONTRIBUTING.md#contributing).
diff --git a/docs/getting-into-the-code/guidelines-for-setting-up-a-local-workspace/intellij-12-build.md b/docs/getting-into-the-code/guidelines-for-setting-up-a-local-workspace/intellij-12-build.md
index 96680ba6de1..b67c0f6fb99 100644
--- a/docs/getting-into-the-code/guidelines-for-setting-up-a-local-workspace/intellij-12-build.md
+++ b/docs/getting-into-the-code/guidelines-for-setting-up-a-local-workspace/intellij-12-build.md
@@ -50,7 +50,7 @@ If that does not exist, just select JDK 21.
![Gradle JVM is project SDK](guidelines-intellij-settings-gradle-gradlejvm-is-projectjvm.png)
{% endfigure %}
-## Enable compiliation by IntelliJ
+## Enable compilation by IntelliJ
To prepare IntelliJ's build system additional steps are required:
diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java
index 852af1ba396..a199ea29502 100644
--- a/src/main/java/module-info.java
+++ b/src/main/java/module-info.java
@@ -163,7 +163,7 @@
/**
* In case the version is updated, please also increment {@link org.jabref.model.search.SearchFieldConstants#VERSION} to trigger reindexing.
*/
- uses org.apache.lucene.codecs.lucene99.Lucene99Codec;
+ uses org.apache.lucene.codecs.lucene100.Lucene100Codec;
requires org.apache.lucene.analysis.common;
requires org.apache.lucene.core;
requires org.apache.lucene.highlighter;
diff --git a/src/main/java/org/jabref/Launcher.java b/src/main/java/org/jabref/Launcher.java
index 947c2f3dd34..8806c7892bf 100644
--- a/src/main/java/org/jabref/Launcher.java
+++ b/src/main/java/org/jabref/Launcher.java
@@ -1,237 +1,44 @@
package org.jabref;
-import java.io.File;
-import java.io.IOException;
-import java.net.Authenticator;
-import java.nio.file.DirectoryStream;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.util.ArrayList;
-import java.util.Comparator;
import java.util.List;
-import java.util.Map;
-import org.jabref.cli.ArgumentProcessor;
-import org.jabref.cli.JabRefCLI;
+import org.jabref.cli.JabKit;
import org.jabref.gui.JabRefGUI;
import org.jabref.gui.preferences.GuiPreferences;
import org.jabref.gui.preferences.JabRefGuiPreferences;
-import org.jabref.gui.util.DefaultDirectoryMonitor;
import org.jabref.gui.util.DefaultFileUpdateMonitor;
import org.jabref.logic.UiCommand;
-import org.jabref.logic.journals.JournalAbbreviationLoader;
-import org.jabref.logic.journals.JournalAbbreviationRepository;
-import org.jabref.logic.net.ProxyAuthenticator;
-import org.jabref.logic.net.ProxyPreferences;
-import org.jabref.logic.net.ProxyRegisterer;
-import org.jabref.logic.net.ssl.SSLPreferences;
-import org.jabref.logic.net.ssl.TrustStoreManager;
import org.jabref.logic.preferences.CliPreferences;
-import org.jabref.logic.protectedterms.ProtectedTermsLoader;
-import org.jabref.logic.remote.RemotePreferences;
-import org.jabref.logic.remote.client.RemoteClient;
-import org.jabref.logic.util.BuildInfo;
-import org.jabref.logic.util.Directories;
import org.jabref.logic.util.HeadlessExecutorService;
import org.jabref.migrations.PreferencesMigrations;
-import org.jabref.model.entry.BibEntryTypesManager;
-import org.jabref.model.util.DirectoryMonitor;
-import org.jabref.model.util.FileUpdateMonitor;
import com.airhacks.afterburner.injection.Injector;
-import org.apache.commons.cli.ParseException;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.slf4j.bridge.SLF4JBridgeHandler;
-import org.tinylog.configuration.Configuration;
-/**
- * The main entry point for the JabRef application.
- *
- * It has two main functions:
- * - Handle the command line arguments
- * - Start the JavaFX application (if not in cli mode)
- */
+/// The main entry point for the JabRef application.
+///
+/// It has two main functions:
+///
+/// - Handle the command line arguments
+/// - Start the JavaFX application (if not in CLI mode)
public class Launcher {
- private static Logger LOGGER;
public static void main(String[] args) {
- initLogging(args);
+ JabKit.initLogging(args);
- try {
- Injector.setModelOrService(BuildInfo.class, new BuildInfo());
+ // Initialize preferences
+ final JabRefGuiPreferences preferences = JabRefGuiPreferences.getInstance();
+ Injector.setModelOrService(CliPreferences.class, preferences);
+ Injector.setModelOrService(GuiPreferences.class, preferences);
- // Initialize preferences
- final JabRefGuiPreferences preferences = JabRefGuiPreferences.getInstance();
- Injector.setModelOrService(CliPreferences.class, preferences);
- Injector.setModelOrService(GuiPreferences.class, preferences);
+ DefaultFileUpdateMonitor fileUpdateMonitor = new DefaultFileUpdateMonitor();
+ HeadlessExecutorService.INSTANCE.executeInterruptableTask(fileUpdateMonitor, "FileUpdateMonitor");
- // Early exit in case another instance is already running
- if (!handleMultipleAppInstances(args, preferences.getRemotePreferences())) {
- return;
- }
+ List uiCommands = JabKit.processArguments(args, preferences, fileUpdateMonitor);
+ // The method `processArguments` quites the whole JVM if no GUI is needed.
- BibEntryTypesManager entryTypesManager = preferences.getCustomEntryTypesRepository();
- Injector.setModelOrService(BibEntryTypesManager.class, entryTypesManager);
+ PreferencesMigrations.runMigrations(preferences);
- PreferencesMigrations.runMigrations(preferences, entryTypesManager);
-
- Injector.setModelOrService(JournalAbbreviationRepository.class, JournalAbbreviationLoader.loadRepository(preferences.getJournalAbbreviationPreferences()));
- Injector.setModelOrService(ProtectedTermsLoader.class, new ProtectedTermsLoader(preferences.getProtectedTermsPreferences()));
-
- configureProxy(preferences.getProxyPreferences());
- configureSSL(preferences.getSSLPreferences());
-
- clearOldSearchIndices();
-
- try {
- DefaultFileUpdateMonitor fileUpdateMonitor = new DefaultFileUpdateMonitor();
- Injector.setModelOrService(FileUpdateMonitor.class, fileUpdateMonitor);
- HeadlessExecutorService.INSTANCE.executeInterruptableTask(fileUpdateMonitor, "FileUpdateMonitor");
-
- DirectoryMonitor directoryMonitor = new DefaultDirectoryMonitor();
- Injector.setModelOrService(DirectoryMonitor.class, directoryMonitor);
-
- // Process arguments
- ArgumentProcessor argumentProcessor = new ArgumentProcessor(
- args,
- ArgumentProcessor.Mode.INITIAL_START,
- preferences,
- preferences,
- fileUpdateMonitor,
- entryTypesManager);
- argumentProcessor.processArguments();
- if (argumentProcessor.shouldShutDown()) {
- LOGGER.debug("JabRef shut down after processing command line arguments");
- // A clean shutdown takes 60s time
- // We don't need the clean shutdown here
- System.exit(0);
- }
-
- List uiCommands = new ArrayList<>(argumentProcessor.getUiCommands());
- JabRefGUI.setup(uiCommands, preferences, fileUpdateMonitor);
- JabRefGUI.launch(JabRefGUI.class, args);
- } catch (ParseException e) {
- LOGGER.error("Problem parsing arguments", e);
- JabRefCLI.printUsage(preferences);
- }
- } catch (Exception ex) {
- LOGGER.error("Unexpected exception", ex);
- }
- }
-
- /**
- * This needs to be called as early as possible. After the first log write, it
- * is not possible to alter the log configuration programmatically anymore.
- */
- private static void initLogging(String[] args) {
- // routeLoggingToSlf4J
- SLF4JBridgeHandler.removeHandlersForRootLogger();
- SLF4JBridgeHandler.install();
-
- // We must configure logging as soon as possible, which is why we cannot wait for the usual
- // argument parsing workflow to parse logging options .e.g. --debug
- boolean isDebugEnabled;
- try {
- JabRefCLI jabRefCLI = new JabRefCLI(args);
- isDebugEnabled = jabRefCLI.isDebugLogging();
- } catch (ParseException e) {
- isDebugEnabled = false;
- }
-
- // addLogToDisk
- Path directory = Directories.getLogDirectory();
- try {
- Files.createDirectories(directory);
- } catch (IOException e) {
- LOGGER = LoggerFactory.getLogger(Launcher.class);
- LOGGER.error("Could not create log directory {}", directory, e);
- return;
- }
-
- // The "Shared File Writer" is explained at
- // https://tinylog.org/v2/configuration/#shared-file-writer
- Map configuration = Map.of(
- "level", isDebugEnabled ? "debug" : "info",
- "writerFile", "rolling file",
- "writerFile.level", isDebugEnabled ? "debug" : "info",
- // We need to manually join the path, because ".resolve" does not work on Windows, because ":" is not allowed in file names on Windows
- "writerFile.file", directory + File.separator + "log_{date:yyyy-MM-dd_HH-mm-ss}.txt",
- "writerFile.charset", "UTF-8",
- "writerFile.policies", "startup",
- "writerFile.backups", "30");
- configuration.forEach(Configuration::set);
-
- LOGGER = LoggerFactory.getLogger(Launcher.class);
- }
-
- /**
- * @return true if JabRef should continue starting up, false if it should quit.
- */
- private static boolean handleMultipleAppInstances(String[] args, RemotePreferences remotePreferences) throws InterruptedException {
- LOGGER.trace("Checking for remote handling...");
- if (remotePreferences.useRemoteServer()) {
- // Try to contact already running JabRef
- RemoteClient remoteClient = new RemoteClient(remotePreferences.getPort());
- if (remoteClient.ping()) {
- LOGGER.debug("Pinging other instance succeeded.");
- if (args.length == 0) {
- // There is already a server out there, avoid showing log "Passing arguments" while no arguments are provided.
- LOGGER.warn("This JabRef instance is already running. Please switch to that instance.");
- } else {
- // We are not alone, there is already a server out there, send command line arguments to other instance
- LOGGER.debug("Passing arguments passed on to running JabRef...");
- if (remoteClient.sendCommandLineArguments(args)) {
- // So we assume it's all taken care of, and quit.
- // Output to both to the log and the screen. Therefore, we do not have an additional System.out.println.
- LOGGER.info("Arguments passed on to running JabRef instance. Shutting down.");
- } else {
- LOGGER.warn("Could not communicate with other running JabRef instance.");
- }
- }
- // We do not launch a new instance in presence if there is another instance running
- return false;
- } else {
- LOGGER.debug("Could not ping JabRef instance.");
- }
- }
- return true;
- }
-
- private static void configureProxy(ProxyPreferences proxyPreferences) {
- ProxyRegisterer.register(proxyPreferences);
- if (proxyPreferences.shouldUseProxy() && proxyPreferences.shouldUseAuthentication()) {
- Authenticator.setDefault(new ProxyAuthenticator());
- }
- }
-
- private static void configureSSL(SSLPreferences sslPreferences) {
- TrustStoreManager.createTruststoreFileIfNotExist(Path.of(sslPreferences.getTruststorePath()));
- }
-
- private static void clearOldSearchIndices() {
- Path currentIndexPath = Directories.getFulltextIndexBaseDirectory();
- Path appData = currentIndexPath.getParent();
-
- try {
- Files.createDirectories(currentIndexPath);
- } catch (IOException e) {
- LOGGER.error("Could not create index directory {}", appData, e);
- }
-
- try (DirectoryStream stream = Files.newDirectoryStream(appData)) {
- for (Path path : stream) {
- if (Files.isDirectory(path) && !path.toString().endsWith("ssl") && path.toString().contains("lucene")
- && !path.equals(currentIndexPath)) {
- LOGGER.info("Deleting out-of-date fulltext search index at {}.", path);
- Files.walk(path)
- .sorted(Comparator.reverseOrder())
- .map(Path::toFile)
- .forEach(File::delete);
- }
- }
- } catch (IOException e) {
- LOGGER.error("Could not access app-directory at {}", appData, e);
- }
+ JabRefGUI.setup(uiCommands, preferences, fileUpdateMonitor);
+ JabRefGUI.launch(JabRefGUI.class, args);
}
}
diff --git a/src/main/java/org/jabref/cli/ArgumentProcessor.java b/src/main/java/org/jabref/cli/ArgumentProcessor.java
index 64edddac7a4..1021049647a 100644
--- a/src/main/java/org/jabref/cli/ArgumentProcessor.java
+++ b/src/main/java/org/jabref/cli/ArgumentProcessor.java
@@ -13,8 +13,6 @@
import java.util.Set;
import java.util.prefs.BackingStoreException;
-import org.jabref.gui.externalfiles.AutoSetFileLinksUtil;
-import org.jabref.gui.preferences.GuiPreferences;
import org.jabref.logic.FilePreferences;
import org.jabref.logic.JabRefException;
import org.jabref.logic.UiCommand;
@@ -70,12 +68,11 @@ public class ArgumentProcessor {
public enum Mode { INITIAL_START, REMOTE_START }
- private final JabRefCLI cli;
+ private final CliOptions cli;
private final Mode startupMode;
private final CliPreferences cliPreferences;
- private final GuiPreferences guiPreferences;
private final FileUpdateMonitor fileUpdateMonitor;
private final BibEntryTypesManager entryTypesManager;
@@ -91,14 +88,12 @@ public enum Mode { INITIAL_START, REMOTE_START }
public ArgumentProcessor(String[] args,
Mode startupMode,
CliPreferences cliPreferences,
- GuiPreferences guiPreferences,
FileUpdateMonitor fileUpdateMonitor,
BibEntryTypesManager entryTypesManager)
throws org.apache.commons.cli.ParseException {
- this.cli = new JabRefCLI(args);
+ this.cli = new CliOptions(args);
this.startupMode = startupMode;
this.cliPreferences = cliPreferences;
- this.guiPreferences = guiPreferences;
this.fileUpdateMonitor = fileUpdateMonitor;
this.entryTypesManager = entryTypesManager;
}
@@ -206,7 +201,7 @@ public void processArguments() {
}
if ((startupMode == Mode.INITIAL_START) && cli.isHelp()) {
- JabRefCLI.printUsage(cliPreferences);
+ CliOptions.printUsage(cliPreferences);
guiNeeded = false;
return;
}
@@ -243,10 +238,6 @@ public void processArguments() {
regenerateCitationKeys(loaded);
}
- if (cli.isAutomaticallySetFileLinks()) {
- automaticallySetFileLinks(loaded);
- }
-
if ((cli.isWriteXmpToPdf() && cli.isEmbedBibFileInPdf()) || (cli.isWriteMetadataToPdf() && (cli.isWriteXmpToPdf() || cli.isEmbedBibFileInPdf()))) {
System.err.println("Give only one of [writeXmpToPdf, embedBibFileInPdf, writeMetadataToPdf]");
}
@@ -258,7 +249,7 @@ public void processArguments() {
cliPreferences.getXmpPreferences(),
cliPreferences.getFilePreferences(),
cliPreferences.getLibraryPreferences().getDefaultBibDatabaseMode(),
- Injector.instantiateModelOrService(BibEntryTypesManager.class),
+ cliPreferences.getCustomEntryTypesRepository(),
cliPreferences.getFieldPreferences(),
Injector.instantiateModelOrService(JournalAbbreviationRepository.class),
cli.isWriteXmpToPdf() || cli.isWriteMetadataToPdf(),
@@ -300,7 +291,7 @@ public void processArguments() {
}
}
- private void writeMetadataToPdf(List loaded,
+ private static void writeMetadataToPdf(List loaded,
String filesAndCiteKeys,
XmpPreferences xmpPreferences,
FilePreferences filePreferences,
@@ -366,7 +357,7 @@ private void writeMetadataToPdf(List loaded,
embeddBibfile);
}
- private void writeMetadataToPDFsOfEntry(BibDatabaseContext databaseContext,
+ private static void writeMetadataToPDFsOfEntry(BibDatabaseContext databaseContext,
String citeKey,
BibEntry entry,
FilePreferences filePreferences,
@@ -395,7 +386,7 @@ private void writeMetadataToPDFsOfEntry(BibDatabaseContext databaseContext,
}
}
- private void writeMetadataToPdfByCitekey(BibDatabaseContext databaseContext,
+ private static void writeMetadataToPdfByCitekey(BibDatabaseContext databaseContext,
List citeKeys,
FilePreferences filePreferences,
XmpPdfExporter xmpPdfExporter,
@@ -415,7 +406,7 @@ private void writeMetadataToPdfByCitekey(BibDatabaseContext databaseContext,
}
}
- private void writeMetadataToPdfByFileNames(BibDatabaseContext databaseContext,
+ private static void writeMetadataToPdfByFileNames(BibDatabaseContext databaseContext,
List pdfs,
FilePreferences filePreferences,
XmpPdfExporter xmpPdfExporter,
@@ -486,7 +477,7 @@ private boolean exportMatches(List loaded) {
formatName = "bib";
default -> {
System.err.println(Localization.lang("Output file missing").concat(". \n \t ")
- .concat(Localization.lang("Usage")).concat(": ") + JabRefCLI.getExportMatchesSyntax());
+ .concat(Localization.lang("Usage")).concat(": ") + CliOptions.getExportMatchesSyntax());
guiNeeded = false;
return false;
}
@@ -499,9 +490,7 @@ private boolean exportMatches(List loaded) {
LOGGER.debug("Finished export");
} else {
// export new database
- ExporterFactory exporterFactory = ExporterFactory.create(
- cliPreferences,
- Injector.instantiateModelOrService(BibEntryTypesManager.class));
+ ExporterFactory exporterFactory = ExporterFactory.create(cliPreferences);
Optional exporter = exporterFactory.getExporterByName(formatName);
if (exporter.isEmpty()) {
System.err.println(Localization.lang("Unknown export format %0", formatName));
@@ -679,9 +668,7 @@ private void exportFile(List loaded, String[] data) {
List fileDirForDatabase = databaseContext
.getFileDirectories(cliPreferences.getFilePreferences());
System.out.println(Localization.lang("Exporting %0", data[0]));
- ExporterFactory exporterFactory = ExporterFactory.create(
- cliPreferences,
- Injector.instantiateModelOrService(BibEntryTypesManager.class));
+ ExporterFactory exporterFactory = ExporterFactory.create(cliPreferences);
Optional exporter = exporterFactory.getExporterByName(data[1]);
if (exporter.isEmpty()) {
System.err.println(Localization.lang("Unknown export format %0", data[1]));
@@ -733,24 +720,6 @@ private void resetPreferences(String value) {
}
}
- private void automaticallySetFileLinks(List loaded) {
- for (ParserResult parserResult : loaded) {
- BibDatabase database = parserResult.getDatabase();
- LOGGER.info("Automatically setting file links for {}",
- parserResult.getDatabaseContext().getDatabasePath()
- .map(Path::getFileName)
- .map(Path::toString).orElse("UNKNOWN"));
-
- AutoSetFileLinksUtil util = new AutoSetFileLinksUtil(
- parserResult.getDatabaseContext(),
- guiPreferences.getExternalApplicationsPreferences(),
- cliPreferences.getFilePreferences(),
- cliPreferences.getAutoLinkPreferences());
-
- util.linkAssociatedFiles(database.getEntries(), (linkedFile, bibEntry) -> bibEntry.addFile(linkedFile));
- }
- }
-
private void regenerateCitationKeys(List loaded) {
for (ParserResult parserResult : loaded) {
BibDatabase database = parserResult.getDatabase();
diff --git a/src/main/java/org/jabref/cli/AuxCommandLine.java b/src/main/java/org/jabref/cli/AuxCommandLine.java
index 61f4c133008..ea566f44e24 100644
--- a/src/main/java/org/jabref/cli/AuxCommandLine.java
+++ b/src/main/java/org/jabref/cli/AuxCommandLine.java
@@ -2,9 +2,9 @@
import java.nio.file.Path;
-import org.jabref.gui.auximport.AuxParserResultViewModel;
import org.jabref.logic.auxparser.AuxParser;
import org.jabref.logic.auxparser.AuxParserResult;
+import org.jabref.logic.auxparser.AuxParserStatisticsProvider;
import org.jabref.logic.auxparser.DefaultAuxParser;
import org.jabref.model.database.BibDatabase;
import org.jabref.model.strings.StringUtil;
@@ -26,7 +26,7 @@ public BibDatabase perform() {
AuxParserResult result = auxParser.parse(Path.of(auxFile));
subDatabase = result.getGeneratedBibDatabase();
// print statistics
- System.out.println(new AuxParserResultViewModel(result).getInformation(true));
+ System.out.println(new AuxParserStatisticsProvider(result).getInformation(true));
}
return subDatabase;
}
diff --git a/src/main/java/org/jabref/cli/JabRefCLI.java b/src/main/java/org/jabref/cli/CliOptions.java
similarity index 80%
rename from src/main/java/org/jabref/cli/JabRefCLI.java
rename to src/main/java/org/jabref/cli/CliOptions.java
index 0cd74a0bba4..9a4d163677c 100644
--- a/src/main/java/org/jabref/cli/JabRefCLI.java
+++ b/src/main/java/org/jabref/cli/CliOptions.java
@@ -11,11 +11,9 @@
import org.jabref.logic.os.OS;
import org.jabref.logic.preferences.CliPreferences;
import org.jabref.logic.util.BuildInfo;
-import org.jabref.model.entry.BibEntryTypesManager;
import org.jabref.model.strings.StringUtil;
import org.jabref.model.util.DummyFileUpdateMonitor;
-import com.airhacks.afterburner.injection.Injector;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.DefaultParser;
import org.apache.commons.cli.HelpFormatter;
@@ -23,18 +21,21 @@
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
-public class JabRefCLI {
+/**
+ * Holds the command line options. It parses it using Apache Commons CLI.
+ */
+public class CliOptions {
private static final int WIDTH = 100; // Number of characters per line before a line break must be added.
private static final String WRAPPED_LINE_PREFIX = ""; // If a line break is added, this prefix will be inserted at the beginning of the next line
private static final String STRING_TABLE_DELIMITER = " : ";
- private final CommandLine cl;
+ private final CommandLine commandLine;
private final List leftOver;
- public JabRefCLI(String[] args) throws ParseException {
+ public CliOptions(String[] args) throws ParseException {
Options options = getOptions();
- this.cl = new DefaultParser().parse(options, args, true);
- this.leftOver = cl.getArgList();
+ this.commandLine = new DefaultParser().parse(options, args, true);
+ this.leftOver = commandLine.getArgList();
}
public static String getExportMatchesSyntax() {
@@ -45,137 +46,133 @@ public static String getExportMatchesSyntax() {
}
public boolean isHelp() {
- return cl.hasOption("help");
+ return commandLine.hasOption("help");
}
public boolean isShowVersion() {
- return cl.hasOption("version");
+ return commandLine.hasOption("version");
}
public boolean isBlank() {
- return cl.hasOption("blank");
+ return commandLine.hasOption("blank");
}
public boolean isDisableGui() {
- return cl.hasOption("nogui");
+ return commandLine.hasOption("nogui");
}
public boolean isPreferencesExport() {
- return cl.hasOption("prexp");
+ return commandLine.hasOption("prexp");
}
public String getPreferencesExport() {
- return cl.getOptionValue("prexp", "jabref_prefs.xml");
+ return commandLine.getOptionValue("prexp", "jabref_prefs.xml");
}
public boolean isPreferencesImport() {
- return cl.hasOption("primp");
+ return commandLine.hasOption("primp");
}
public String getPreferencesImport() {
- return cl.getOptionValue("primp", "jabref_prefs.xml");
+ return commandLine.getOptionValue("primp", "jabref_prefs.xml");
}
public boolean isPreferencesReset() {
- return cl.hasOption("prdef");
+ return commandLine.hasOption("prdef");
}
public String getPreferencesReset() {
- return cl.getOptionValue("prdef");
+ return commandLine.getOptionValue("prdef");
}
public boolean isFileExport() {
- return cl.hasOption("output");
+ return commandLine.hasOption("output");
}
public String getFileExport() {
- return cl.getOptionValue("output");
+ return commandLine.getOptionValue("output");
}
public boolean isBibtexImport() {
- return cl.hasOption("importBibtex");
+ return commandLine.hasOption("importBibtex");
}
public String getBibtexImport() {
- return cl.getOptionValue("importBibtex");
+ return commandLine.getOptionValue("importBibtex");
}
public boolean isFileImport() {
- return cl.hasOption("import");
+ return commandLine.hasOption("import");
}
public String getFileImport() {
- return cl.getOptionValue("import");
+ return commandLine.getOptionValue("import");
}
public boolean isAuxImport() {
- return cl.hasOption("aux");
+ return commandLine.hasOption("aux");
}
public String getAuxImport() {
- return cl.getOptionValue("aux");
+ return commandLine.getOptionValue("aux");
}
public boolean isImportToOpenBase() {
- return cl.hasOption("importToOpen");
+ return commandLine.hasOption("importToOpen");
}
public String getImportToOpenBase() {
- return cl.getOptionValue("importToOpen");
+ return commandLine.getOptionValue("importToOpen");
}
public boolean isDebugLogging() {
- return cl.hasOption("debug");
+ return commandLine.hasOption("debug");
}
public boolean isFetcherEngine() {
- return cl.hasOption("fetch");
+ return commandLine.hasOption("fetch");
}
public String getFetcherEngine() {
- return cl.getOptionValue("fetch");
+ return commandLine.getOptionValue("fetch");
}
public boolean isExportMatches() {
- return cl.hasOption("exportMatches");
+ return commandLine.hasOption("exportMatches");
}
public String getExportMatches() {
- return cl.getOptionValue("exportMatches");
+ return commandLine.getOptionValue("exportMatches");
}
public boolean isGenerateCitationKeys() {
- return cl.hasOption("generateCitationKeys");
- }
-
- public boolean isAutomaticallySetFileLinks() {
- return cl.hasOption("automaticallySetFileLinks");
+ return commandLine.hasOption("generateCitationKeys");
}
public boolean isWriteXmpToPdf() {
- return cl.hasOption("writeXmpToPdf");
+ return commandLine.hasOption("writeXmpToPdf");
}
public boolean isEmbedBibFileInPdf() {
- return cl.hasOption("embedBibFileInPdf");
+ return commandLine.hasOption("embedBibFileInPdf");
}
public boolean isWriteMetadataToPdf() {
- return cl.hasOption("writeMetadataToPdf");
+ return commandLine.hasOption("writeMetadataToPdf");
}
public String getWriteMetadataToPdf() {
- return cl.hasOption("writeMetadatatoPdf") ? cl.getOptionValue("writeMetadataToPdf") :
- cl.hasOption("writeXMPtoPdf") ? cl.getOptionValue("writeXmpToPdf") :
- cl.hasOption("embeddBibfileInPdf") ? cl.getOptionValue("embeddBibfileInPdf") : null;
+ return commandLine.hasOption("writeMetadatatoPdf") ? commandLine.getOptionValue("writeMetadataToPdf") :
+ commandLine.hasOption("writeXMPtoPdf") ? commandLine.getOptionValue("writeXmpToPdf") :
+ commandLine.hasOption("embeddBibfileInPdf") ? commandLine.getOptionValue("embeddBibfileInPdf") : null;
}
public String getJumpToKey() {
- return cl.getOptionValue("jumpToKey");
+ return commandLine.getOptionValue("jumpToKey");
}
public boolean isJumpToKey() {
- return cl.hasOption("jumpToKey");
+ return commandLine.hasOption("jumpToKey");
}
private static Options getOptions() {
@@ -184,7 +181,6 @@ private static Options getOptions() {
// boolean options
options.addOption("h", "help", false, Localization.lang("Display help on command line options"));
options.addOption("n", "nogui", false, Localization.lang("No GUI. Only process command line options"));
- options.addOption("asfl", "automaticallySetFileLinks", false, Localization.lang("Automatically set file links"));
options.addOption("g", "generateCitationKeys", false, Localization.lang("Regenerate all keys for the entries in a BibTeX file"));
options.addOption("b", "blank", false, Localization.lang("Do not open any files at startup"));
options.addOption("v", "version", false, Localization.lang("Display version"));
@@ -211,7 +207,7 @@ private static Options getOptions() {
.longOpt("importBibtex")
.desc("%s: '%s'".formatted(Localization.lang("Import BibTeX"), "-ib @article{entry}"))
.hasArg()
- .argName("BIBTEXT_STRING")
+ .argName("BIBTEX_STRING")
.build());
options.addOption(Option
@@ -325,9 +321,7 @@ public static void printUsage(CliPreferences preferences) {
String importFormatsIntro = Localization.lang("Available import formats");
String importFormatsList = "%s:%n%s%n".formatted(importFormatsIntro, alignStringTable(importFormats));
- ExporterFactory exporterFactory = ExporterFactory.create(
- preferences,
- Injector.instantiateModelOrService(BibEntryTypesManager.class));
+ ExporterFactory exporterFactory = ExporterFactory.create(preferences);
List> exportFormats = exporterFactory
.getExporters().stream()
.map(format -> new Pair<>(format.getName(), format.getId()))
@@ -342,8 +336,7 @@ public static void printUsage(CliPreferences preferences) {
}
private String getVersionInfo() {
- BuildInfo buildInfo = Injector.instantiateModelOrService(BuildInfo.class);
- return "JabRef %s".formatted(buildInfo.version);
+ return "JabRef %s".formatted(new BuildInfo().version);
}
public List getLeftOver() {
diff --git a/src/main/java/org/jabref/cli/JabKit.java b/src/main/java/org/jabref/cli/JabKit.java
new file mode 100644
index 00000000000..72cab155979
--- /dev/null
+++ b/src/main/java/org/jabref/cli/JabKit.java
@@ -0,0 +1,241 @@
+package org.jabref.cli;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.Authenticator;
+import java.nio.file.DirectoryStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+
+import org.jabref.logic.UiCommand;
+import org.jabref.logic.journals.JournalAbbreviationLoader;
+import org.jabref.logic.journals.JournalAbbreviationRepository;
+import org.jabref.logic.net.ProxyAuthenticator;
+import org.jabref.logic.net.ProxyPreferences;
+import org.jabref.logic.net.ProxyRegisterer;
+import org.jabref.logic.net.ssl.SSLPreferences;
+import org.jabref.logic.net.ssl.TrustStoreManager;
+import org.jabref.logic.preferences.CliPreferences;
+import org.jabref.logic.preferences.JabRefCliPreferences;
+import org.jabref.logic.protectedterms.ProtectedTermsLoader;
+import org.jabref.logic.remote.RemotePreferences;
+import org.jabref.logic.remote.client.RemoteClient;
+import org.jabref.logic.util.BuildInfo;
+import org.jabref.logic.util.Directories;
+import org.jabref.model.entry.BibEntryTypesManager;
+import org.jabref.model.util.DummyFileUpdateMonitor;
+import org.jabref.model.util.FileUpdateMonitor;
+
+import com.airhacks.afterburner.injection.Injector;
+import org.apache.commons.cli.ParseException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.slf4j.bridge.SLF4JBridgeHandler;
+import org.tinylog.configuration.Configuration;
+
+/// Entrypoint for a command-line only version of JabRef.
+/// It does not open any dialogs, just parses the command line arguments and outputs text and creates/modifies files.
+///
+/// See [Command Line Interface Guidelines](https://clig.dev/) for general guidelines how to design a good CLI interface.
+///
+/// It does not open any GUI.
+/// For the GUI application see {@link org.jabref.Launcher}.
+///
+/// Does not do any preference migrations.
+public class JabKit {
+ private static Logger LOGGER;
+
+ public static void main(String[] args) {
+ initLogging(args);
+
+ final JabRefCliPreferences preferences = JabRefCliPreferences.getInstance();
+ Injector.setModelOrService(CliPreferences.class, preferences);
+
+ FileUpdateMonitor fileUpdateMonitor = new DummyFileUpdateMonitor();
+
+ List uiCommands = processArguments(args, preferences, fileUpdateMonitor);
+ if (!uiCommands.isEmpty()) {
+ LOGGER.error("No GUI needed, but UI commands were returned. Exiting.");
+ }
+ }
+
+ private static void systemExit() {
+ LOGGER.debug("JabRef shut down after processing command line arguments");
+ // A clean shutdown takes 60s time
+ // We don't need the clean shutdown here
+ System.exit(0);
+ }
+
+ public static List processArguments(String[] args, JabRefCliPreferences preferences, FileUpdateMonitor fileUpdateMonitor) {
+ try {
+ Injector.setModelOrService(BuildInfo.class, new BuildInfo());
+
+ // Early exit in case another instance is already running
+ if (!handleMultipleAppInstances(args, preferences.getRemotePreferences())) {
+ systemExit();
+ }
+
+ BibEntryTypesManager entryTypesManager = preferences.getCustomEntryTypesRepository();
+ Injector.setModelOrService(BibEntryTypesManager.class, entryTypesManager);
+
+ Injector.setModelOrService(JournalAbbreviationRepository.class, JournalAbbreviationLoader.loadRepository(preferences.getJournalAbbreviationPreferences()));
+ Injector.setModelOrService(ProtectedTermsLoader.class, new ProtectedTermsLoader(preferences.getProtectedTermsPreferences()));
+
+ configureProxy(preferences.getProxyPreferences());
+ configureSSL(preferences.getSSLPreferences());
+
+ clearOldSearchIndices();
+
+ try {
+ Injector.setModelOrService(FileUpdateMonitor.class, fileUpdateMonitor);
+
+ // Process arguments
+ ArgumentProcessor argumentProcessor = new ArgumentProcessor(
+ args,
+ ArgumentProcessor.Mode.INITIAL_START,
+ preferences,
+ fileUpdateMonitor,
+ entryTypesManager);
+ argumentProcessor.processArguments();
+ if (argumentProcessor.shouldShutDown()) {
+ LOGGER.debug("JabRef shut down after processing command line arguments");
+ // A clean shutdown takes 60s time
+ // We don't need the clean shutdown here
+ System.exit(0);
+ return null;
+ }
+
+ return new ArrayList<>(argumentProcessor.getUiCommands());
+ } catch (ParseException e) {
+ LOGGER.error("Problem parsing arguments", e);
+ CliOptions.printUsage(preferences);
+ systemExit();
+ return null;
+ }
+ } catch (Exception ex) {
+ LOGGER.error("Unexpected exception", ex);
+ systemExit();
+ return null;
+ }
+ }
+
+ /**
+ * This needs to be called as early as possible. After the first log write, it
+ * is not possible to alter the log configuration programmatically anymore.
+ */
+ public static void initLogging(String[] args) {
+ // routeLoggingToSlf4J
+ SLF4JBridgeHandler.removeHandlersForRootLogger();
+ SLF4JBridgeHandler.install();
+
+ // We must configure logging as soon as possible, which is why we cannot wait for the usual
+ // argument parsing workflow to parse logging options .e.g. --debug
+ boolean isDebugEnabled;
+ try {
+ CliOptions cliOptions = new CliOptions(args);
+ isDebugEnabled = cliOptions.isDebugLogging();
+ } catch (ParseException e) {
+ isDebugEnabled = false;
+ }
+
+ // addLogToDisk
+ // We cannot use `Injector.instantiateModelOrService(BuildInfo.class).version` here, because this initializes logging
+ Path directory = Directories.getLogDirectory(new BuildInfo().version);
+ try {
+ Files.createDirectories(directory);
+ } catch (IOException e) {
+ LOGGER = LoggerFactory.getLogger(JabKit.class);
+ LOGGER.error("Could not create log directory {}", directory, e);
+ return;
+ }
+
+ // The "Shared File Writer" is explained at
+ // https://tinylog.org/v2/configuration/#shared-file-writer
+ Map configuration = Map.of(
+ "level", isDebugEnabled ? "debug" : "info",
+ "writerFile", "rolling file",
+ "writerFile.level", isDebugEnabled ? "debug" : "info",
+ // We need to manually join the path, because ".resolve" does not work on Windows, because ":" is not allowed in file names on Windows
+ "writerFile.file", directory + File.separator + "log_{date:yyyy-MM-dd_HH-mm-ss}.txt",
+ "writerFile.charset", "UTF-8",
+ "writerFile.policies", "startup",
+ "writerFile.backups", "30");
+ configuration.forEach(Configuration::set);
+
+ LOGGER = LoggerFactory.getLogger(JabKit.class);
+ }
+
+ /**
+ * @return true if JabRef should continue starting up, false if it should quit.
+ */
+ private static boolean handleMultipleAppInstances(String[] args, RemotePreferences remotePreferences) throws InterruptedException {
+ LOGGER.trace("Checking for remote handling...");
+ if (remotePreferences.useRemoteServer()) {
+ // Try to contact already running JabRef
+ RemoteClient remoteClient = new RemoteClient(remotePreferences.getPort());
+ if (remoteClient.ping()) {
+ LOGGER.debug("Pinging other instance succeeded.");
+ if (args.length == 0) {
+ // There is already a server out there, avoid showing log "Passing arguments" while no arguments are provided.
+ LOGGER.warn("This JabRef instance is already running. Please switch to that instance.");
+ } else {
+ // We are not alone, there is already a server out there, send command line arguments to other instance
+ LOGGER.debug("Passing arguments passed on to running JabRef...");
+ if (remoteClient.sendCommandLineArguments(args)) {
+ // So we assume it's all taken care of, and quit.
+ // Output to both to the log and the screen. Therefore, we do not have an additional System.out.println.
+ LOGGER.info("Arguments passed on to running JabRef instance. Shutting down.");
+ } else {
+ LOGGER.warn("Could not communicate with other running JabRef instance.");
+ }
+ }
+ // We do not launch a new instance in presence if there is another instance running
+ return false;
+ } else {
+ LOGGER.debug("Could not ping JabRef instance.");
+ }
+ }
+ return true;
+ }
+
+ private static void configureProxy(ProxyPreferences proxyPreferences) {
+ ProxyRegisterer.register(proxyPreferences);
+ if (proxyPreferences.shouldUseProxy() && proxyPreferences.shouldUseAuthentication()) {
+ Authenticator.setDefault(new ProxyAuthenticator());
+ }
+ }
+
+ private static void configureSSL(SSLPreferences sslPreferences) {
+ TrustStoreManager.createTruststoreFileIfNotExist(Path.of(sslPreferences.getTruststorePath()));
+ }
+
+ private static void clearOldSearchIndices() {
+ Path currentIndexPath = Directories.getFulltextIndexBaseDirectory();
+ Path appData = currentIndexPath.getParent();
+
+ try {
+ Files.createDirectories(currentIndexPath);
+ } catch (IOException e) {
+ LOGGER.error("Could not create index directory {}", appData, e);
+ }
+
+ try (DirectoryStream stream = Files.newDirectoryStream(appData)) {
+ for (Path path : stream) {
+ if (Files.isDirectory(path) && !path.toString().endsWith("ssl") && path.toString().contains("lucene")
+ && !path.equals(currentIndexPath)) {
+ LOGGER.info("Deleting out-of-date fulltext search index at {}.", path);
+ Files.walk(path)
+ .sorted(Comparator.reverseOrder())
+ .map(Path::toFile)
+ .forEach(File::delete);
+ }
+ }
+ } catch (IOException e) {
+ LOGGER.error("Could not access app-directory at {}", appData, e);
+ }
+ }
+}
diff --git a/src/main/java/org/jabref/gui/JabRefGUI.java b/src/main/java/org/jabref/gui/JabRefGUI.java
index b0c0e1c83f0..94c223eb6c0 100644
--- a/src/main/java/org/jabref/gui/JabRefGUI.java
+++ b/src/main/java/org/jabref/gui/JabRefGUI.java
@@ -14,7 +14,6 @@
import javafx.stage.Stage;
import javafx.stage.WindowEvent;
-import org.jabref.gui.ai.chatting.chathistory.ChatHistoryService;
import org.jabref.gui.frame.JabRefFrame;
import org.jabref.gui.help.VersionWorker;
import org.jabref.gui.icon.IconTheme;
@@ -25,6 +24,7 @@
import org.jabref.gui.remote.CLIMessageHandler;
import org.jabref.gui.theme.ThemeManager;
import org.jabref.gui.undo.CountingUndoManager;
+import org.jabref.gui.util.DefaultDirectoryMonitor;
import org.jabref.gui.util.UiTaskExecutor;
import org.jabref.logic.UiCommand;
import org.jabref.logic.ai.AiService;
@@ -61,7 +61,6 @@ public class JabRefGUI extends Application {
// AI Service handles chat messages etc. Therefore, it is tightly coupled to the GUI.
private static AiService aiService;
- private static ChatHistoryService chatHistoryService;
private static StateManager stateManager;
private static ThemeManager themeManager;
@@ -102,7 +101,6 @@ public void start(Stage stage) {
fileUpdateMonitor,
preferences,
aiService,
- chatHistoryService,
stateManager,
countingUndoManager,
Injector.instantiateModelOrService(BibEntryTypesManager.class),
@@ -137,6 +135,9 @@ public void start(Stage stage) {
public void initialize() {
WebViewStore.init();
+ DirectoryMonitor directoryMonitor = new DefaultDirectoryMonitor();
+ Injector.setModelOrService(DirectoryMonitor.class, directoryMonitor);
+
JabRefGUI.remoteListenerServerManager = new RemoteListenerServerManager();
Injector.setModelOrService(RemoteListenerServerManager.class, remoteListenerServerManager);
@@ -168,14 +169,10 @@ public void initialize() {
JabRefGUI.aiService = new AiService(
preferences.getAiPreferences(),
preferences.getFilePreferences(),
+ preferences.getCitationKeyPatternPreferences(),
dialogService,
taskExecutor);
Injector.setModelOrService(AiService.class, aiService);
-
- JabRefGUI.chatHistoryService = new ChatHistoryService(
- preferences.getCitationKeyPatternPreferences(),
- dialogService);
- Injector.setModelOrService(ChatHistoryService.class, chatHistoryService);
}
private void setupProxy() {
@@ -375,8 +372,6 @@ public void stop() {
} catch (Exception e) {
LOGGER.error("Unable to close AI service", e);
}
- LOGGER.trace("Closing chat history service");
- chatHistoryService.close();
LOGGER.trace("Closing OpenOffice connection");
OOBibBaseConnect.closeOfficeConnection();
LOGGER.trace("Stopping background tasks");
diff --git a/src/main/java/org/jabref/gui/LibraryTab.java b/src/main/java/org/jabref/gui/LibraryTab.java
index 791eec0dee1..3877db193e0 100644
--- a/src/main/java/org/jabref/gui/LibraryTab.java
+++ b/src/main/java/org/jabref/gui/LibraryTab.java
@@ -64,6 +64,7 @@
import org.jabref.gui.undo.UndoableRemoveEntries;
import org.jabref.gui.util.OptionalObjectProperty;
import org.jabref.gui.util.UiTaskExecutor;
+import org.jabref.logic.ai.AiService;
import org.jabref.logic.citationstyle.CitationStyleCache;
import org.jabref.logic.importer.FetcherClientException;
import org.jabref.logic.importer.FetcherException;
@@ -169,6 +170,8 @@ private enum PanelMode { MAIN_TABLE, MAIN_TABLE_AND_ENTRY_EDITOR }
private ImportHandler importHandler;
private LuceneManager luceneManager;
+ private final AiService aiService;
+
/**
* @param isDummyContext Indicates whether the database context is a dummy. A dummy context is used to display a progress indicator while parsing the database.
* If the context is a dummy, the Lucene index should not be created, as both the dummy context and the actual context share the same index path {@link BibDatabaseContext#getFulltextIndexPath()}.
@@ -178,6 +181,7 @@ private enum PanelMode { MAIN_TABLE, MAIN_TABLE_AND_ENTRY_EDITOR }
private LibraryTab(BibDatabaseContext bibDatabaseContext,
LibraryTabContainer tabContainer,
DialogService dialogService,
+ AiService aiService,
GuiPreferences preferences,
StateManager stateManager,
FileUpdateMonitor fileUpdateMonitor,
@@ -197,6 +201,7 @@ private LibraryTab(BibDatabaseContext bibDatabaseContext,
this.clipBoardManager = clipBoardManager;
this.taskExecutor = taskExecutor;
this.directoryMonitorManager = new DirectoryMonitorManager(Injector.instantiateModelOrService(DirectoryMonitor.class));
+ this.aiService = aiService;
initializeComponentsAndListeners(isDummyContext);
@@ -249,6 +254,8 @@ private void initializeComponentsAndListeners(boolean isDummyContext) {
this.entryEditor = createEntryEditor();
+ aiService.setupDatabase(bibDatabaseContext);
+
Platform.runLater(() -> {
EasyBind.subscribe(changedProperty, this::updateTabTitle);
stateManager.getOpenDatabases().addListener((ListChangeListener) c ->
@@ -1052,6 +1059,7 @@ public void resetChangedProperties() {
public static LibraryTab createLibraryTab(BackgroundTask dataLoadingTask,
Path file,
DialogService dialogService,
+ AiService aiService,
GuiPreferences preferences,
StateManager stateManager,
LibraryTabContainer tabContainer,
@@ -1067,6 +1075,7 @@ public static LibraryTab createLibraryTab(BackgroundTask dataLoadi
context,
tabContainer,
dialogService,
+ aiService,
preferences,
stateManager,
fileUpdateMonitor,
@@ -1088,6 +1097,7 @@ public static LibraryTab createLibraryTab(BackgroundTask dataLoadi
public static LibraryTab createLibraryTab(BibDatabaseContext databaseContext,
LibraryTabContainer tabContainer,
DialogService dialogService,
+ AiService aiService,
GuiPreferences preferences,
StateManager stateManager,
FileUpdateMonitor fileUpdateMonitor,
@@ -1101,6 +1111,7 @@ public static LibraryTab createLibraryTab(BibDatabaseContext databaseContext,
databaseContext,
tabContainer,
dialogService,
+ aiService,
preferences,
stateManager,
fileUpdateMonitor,
diff --git a/src/main/java/org/jabref/gui/actions/StandardActions.java b/src/main/java/org/jabref/gui/actions/StandardActions.java
index 7f44fb8810f..df789a51733 100644
--- a/src/main/java/org/jabref/gui/actions/StandardActions.java
+++ b/src/main/java/org/jabref/gui/actions/StandardActions.java
@@ -136,8 +136,6 @@ public enum StandardActions implements Action {
NEW_ENTRY(Localization.lang("New entry"), IconTheme.JabRefIcons.ADD_ENTRY, KeyBinding.NEW_ENTRY),
NEW_ARTICLE(Localization.lang("New article"), IconTheme.JabRefIcons.ADD_ARTICLE),
NEW_ENTRY_FROM_PLAIN_TEXT(Localization.lang("New entry from plain text"), IconTheme.JabRefIcons.NEW_ENTRY_FROM_PLAIN_TEXT),
- NEW_ENTRY_FROM_PLAIN_TEXT_ONLINE(Localization.lang("New entry from plain text (online)"), IconTheme.JabRefIcons.NEW_ENTRY_FROM_PLAIN_TEXT, KeyBinding.NEW_ENTRY_FROM_PLAIN_TEXT_ONLINE),
- NEW_ENTRY_FROM_PLAIN_TEXT_OFFLINE(Localization.lang("New entry from plain text (offline)"), IconTheme.JabRefIcons.NEW_ENTRY_FROM_PLAIN_TEXT),
LIBRARY_PROPERTIES(Localization.lang("Library properties")),
FIND_DUPLICATES(Localization.lang("Find duplicates"), IconTheme.JabRefIcons.FIND_DUPLICATES),
MERGE_ENTRIES(Localization.lang("Merge entries"), IconTheme.JabRefIcons.MERGE_ENTRIES, KeyBinding.MERGE_ENTRIES),
@@ -195,6 +193,8 @@ public enum StandardActions implements Action {
GROUP_REMOVE_WITH_SUBGROUPS(Localization.lang("Also remove subgroups")),
GROUP_CHAT(Localization.lang("Chat with group")),
GROUP_EDIT(Localization.lang("Edit group")),
+ GROUP_GENERATE_SUMMARIES(Localization.lang("Generate summaries for entries in the group")),
+ GROUP_GENERATE_EMBEDDINGS(Localization.lang("Generate embeddings for linked files in the group")),
GROUP_SUBGROUP_ADD(Localization.lang("Add subgroup")),
GROUP_SUBGROUP_REMOVE(Localization.lang("Remove subgroups")),
GROUP_SUBGROUP_SORT(Localization.lang("Sort subgroups A-Z")),
diff --git a/src/main/java/org/jabref/gui/ai/components/privacynotice/PrivacyNoticeComponent.java b/src/main/java/org/jabref/gui/ai/components/privacynotice/PrivacyNoticeComponent.java
index 9681379d60d..12dad69d580 100644
--- a/src/main/java/org/jabref/gui/ai/components/privacynotice/PrivacyNoticeComponent.java
+++ b/src/main/java/org/jabref/gui/ai/components/privacynotice/PrivacyNoticeComponent.java
@@ -11,7 +11,6 @@
import org.jabref.gui.DialogService;
import org.jabref.gui.desktop.os.NativeDesktop;
import org.jabref.gui.frame.ExternalApplicationsPreferences;
-import org.jabref.logic.ai.AiDefaultPreferences;
import org.jabref.logic.ai.AiPreferences;
import org.jabref.model.ai.AiProvider;
@@ -72,12 +71,11 @@ private void initPrivacyHyperlink(TextFlow textFlow, AiProvider aiProvider) {
text.setText(replacedText);
text.wrappingWidthProperty().bind(this.widthProperty());
- String link = AiDefaultPreferences.PROVIDERS_PRIVACY_POLICIES.get(aiProvider);
- Hyperlink hyperlink = new Hyperlink(link);
+ Hyperlink hyperlink = new Hyperlink(aiProvider.getApiUrl());
hyperlink.setWrapText(true);
hyperlink.setFont(text.getFont());
hyperlink.setOnAction(event -> {
- openBrowser(link);
+ openBrowser(aiProvider.getApiUrl());
});
textFlow.getChildren().add(hyperlink);
diff --git a/src/main/java/org/jabref/gui/auximport/FromAuxDialogViewModel.java b/src/main/java/org/jabref/gui/auximport/FromAuxDialogViewModel.java
index eb871f44a22..4f197767dde 100644
--- a/src/main/java/org/jabref/gui/auximport/FromAuxDialogViewModel.java
+++ b/src/main/java/org/jabref/gui/auximport/FromAuxDialogViewModel.java
@@ -20,6 +20,7 @@
import org.jabref.gui.util.FileDialogConfiguration;
import org.jabref.logic.auxparser.AuxParser;
import org.jabref.logic.auxparser.AuxParserResult;
+import org.jabref.logic.auxparser.AuxParserStatisticsProvider;
import org.jabref.logic.auxparser.DefaultAuxParser;
import org.jabref.logic.l10n.Localization;
import org.jabref.logic.preferences.CliPreferences;
@@ -96,7 +97,7 @@ public void parse() {
AuxParser auxParser = new DefaultAuxParser(referenceDatabase);
auxParserResult = auxParser.parse(Path.of(auxName));
notFoundList.setAll(auxParserResult.getUnresolvedKeys());
- statusTextProperty.set(new AuxParserResultViewModel(auxParserResult).getInformation(false));
+ statusTextProperty.set(new AuxParserStatisticsProvider(auxParserResult).getInformation(false));
if (!auxParserResult.getGeneratedBibDatabase().hasEntries()) {
// The generated database contains no entries -> no active generate-button
diff --git a/src/main/java/org/jabref/gui/bibtexextractor/BibtexExtractorViewModel.java b/src/main/java/org/jabref/gui/bibtexextractor/BibtexExtractorViewModel.java
deleted file mode 100644
index 4e1ea80e0e1..00000000000
--- a/src/main/java/org/jabref/gui/bibtexextractor/BibtexExtractorViewModel.java
+++ /dev/null
@@ -1,102 +0,0 @@
-package org.jabref.gui.bibtexextractor;
-
-import java.util.List;
-
-import javax.swing.undo.UndoManager;
-
-import javafx.beans.property.SimpleStringProperty;
-import javafx.beans.property.StringProperty;
-
-import org.jabref.gui.DialogService;
-import org.jabref.gui.StateManager;
-import org.jabref.gui.externalfiles.ImportHandler;
-import org.jabref.gui.preferences.GuiPreferences;
-import org.jabref.logic.importer.FetcherException;
-import org.jabref.logic.importer.fetcher.GrobidCitationFetcher;
-import org.jabref.logic.l10n.Localization;
-import org.jabref.logic.preferences.CliPreferences;
-import org.jabref.logic.util.BackgroundTask;
-import org.jabref.logic.util.TaskExecutor;
-import org.jabref.model.database.BibDatabaseContext;
-import org.jabref.model.entry.BibEntry;
-import org.jabref.model.util.FileUpdateMonitor;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * View model for the feature "Extract BibTeX from plain text".
- * Handles both online and offline case.
- *
- * @implNote Instead of using inheritance, we do if/else checks.
- */
-public class BibtexExtractorViewModel {
-
- private static final Logger LOGGER = LoggerFactory.getLogger(BibtexExtractorViewModel.class);
-
- private final boolean onlineMode;
- private final DialogService dialogService;
- private final CliPreferences preferences;
- private final TaskExecutor taskExecutor;
-
- private final ImportHandler importHandler;
- private final StringProperty inputTextProperty = new SimpleStringProperty("");
-
- public BibtexExtractorViewModel(boolean onlineMode,
- BibDatabaseContext bibdatabaseContext,
- DialogService dialogService,
- GuiPreferences preferences,
- FileUpdateMonitor fileUpdateMonitor,
- TaskExecutor taskExecutor,
- UndoManager undoManager,
- StateManager stateManager) {
- this.onlineMode = onlineMode;
- this.dialogService = dialogService;
- this.preferences = preferences;
- this.taskExecutor = taskExecutor;
- this.importHandler = new ImportHandler(
- bibdatabaseContext,
- preferences,
- fileUpdateMonitor,
- undoManager,
- stateManager,
- dialogService,
- taskExecutor);
- }
-
- public void startParsing() {
- if (onlineMode) {
- startParsingOnline();
- } else {
- startParsingOffline();
- }
- }
-
- private void startParsingOffline() {
- BibEntry parsedEntry = new BibtexExtractor().extract(inputTextProperty.getValue());
- importHandler.importEntries(List.of(parsedEntry));
- }
-
- private void startParsingOnline() {
- GrobidCitationFetcher grobidCitationFetcher = new GrobidCitationFetcher(preferences.getGrobidPreferences(), preferences.getImportFormatPreferences());
- BackgroundTask.wrap(() -> grobidCitationFetcher.performSearch(inputTextProperty.getValue()))
- .onRunning(() -> dialogService.notify(Localization.lang("Your text is being parsed...")))
- .onFailure(e -> {
- if (e instanceof FetcherException) {
- String msg = Localization.lang("There are connection issues with a JabRef server. Detailed information: %0",
- e.getMessage());
- dialogService.notify(msg);
- } else {
- LOGGER.warn("Missing exception handling.", e);
- }
- })
- .onSuccess(parsedEntries -> {
- dialogService.notify(Localization.lang("%0 entries were parsed from your query.", String.valueOf(parsedEntries.size())));
- importHandler.importEntries(parsedEntries);
- }).executeWith(taskExecutor);
- }
-
- public StringProperty inputTextProperty() {
- return this.inputTextProperty;
- }
-}
diff --git a/src/main/java/org/jabref/gui/bibtexextractor/ExtractBibtexActionOnline.java b/src/main/java/org/jabref/gui/bibtexextractor/ExtractBibtexActionOnline.java
deleted file mode 100644
index 043076067e4..00000000000
--- a/src/main/java/org/jabref/gui/bibtexextractor/ExtractBibtexActionOnline.java
+++ /dev/null
@@ -1,37 +0,0 @@
-package org.jabref.gui.bibtexextractor;
-
-import javafx.beans.binding.Bindings;
-
-import org.jabref.gui.DialogService;
-import org.jabref.gui.StateManager;
-import org.jabref.gui.actions.SimpleCommand;
-import org.jabref.gui.importer.GrobidOptInDialogHelper;
-import org.jabref.logic.preferences.CliPreferences;
-
-import static org.jabref.gui.actions.ActionHelper.needsDatabase;
-
-public class ExtractBibtexActionOnline extends SimpleCommand {
-
- private final CliPreferences preferences;
- private final DialogService dialogService;
-
- public ExtractBibtexActionOnline(DialogService dialogService, CliPreferences preferences, StateManager stateManager, boolean requiresGrobid) {
- this.preferences = preferences;
- this.dialogService = dialogService;
- if (requiresGrobid) {
- this.executable.bind(
- Bindings.and(
- preferences.getGrobidPreferences().grobidEnabledProperty(),
- needsDatabase(stateManager)
- ));
- } else {
- this.executable.bind(needsDatabase(stateManager));
- }
- }
-
- @Override
- public void execute() {
- boolean useGrobid = GrobidOptInDialogHelper.showAndWaitIfUserIsUndecided(dialogService, preferences.getGrobidPreferences());
- dialogService.showCustomDialogAndWait(new ExtractBibtexDialog(useGrobid));
- }
-}
diff --git a/src/main/java/org/jabref/gui/bibtexextractor/ExtractBibtexDialog.fxml b/src/main/java/org/jabref/gui/bibtexextractor/ExtractBibtexDialog.fxml
deleted file mode 100644
index eaad07b1bb8..00000000000
--- a/src/main/java/org/jabref/gui/bibtexextractor/ExtractBibtexDialog.fxml
+++ /dev/null
@@ -1,18 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/main/java/org/jabref/gui/edit/CopyDoiUrlAction.java b/src/main/java/org/jabref/gui/edit/CopyDoiUrlAction.java
index 7dec0abeae1..f8d7f173acd 100644
--- a/src/main/java/org/jabref/gui/edit/CopyDoiUrlAction.java
+++ b/src/main/java/org/jabref/gui/edit/CopyDoiUrlAction.java
@@ -2,7 +2,7 @@
import java.util.Optional;
-import javafx.scene.control.TextArea;
+import javafx.scene.control.TextField;
import org.jabref.gui.ClipBoardManager;
import org.jabref.gui.DialogService;
@@ -16,12 +16,12 @@
*/
public class CopyDoiUrlAction extends SimpleCommand {
- private final TextArea component;
+ private final TextField component;
private final StandardActions action;
private final DialogService dialogService;
private final ClipBoardManager clipBoardManager;
- public CopyDoiUrlAction(TextArea component, StandardActions action, DialogService dialogService, ClipBoardManager clipBoardManager) {
+ public CopyDoiUrlAction(TextField component, StandardActions action, DialogService dialogService, ClipBoardManager clipBoardManager) {
this.component = component;
this.action = action;
this.dialogService = dialogService;
diff --git a/src/main/java/org/jabref/gui/entryeditor/AiChatTab.java b/src/main/java/org/jabref/gui/entryeditor/AiChatTab.java
index 95e2b7bf102..29fb54f544c 100644
--- a/src/main/java/org/jabref/gui/entryeditor/AiChatTab.java
+++ b/src/main/java/org/jabref/gui/entryeditor/AiChatTab.java
@@ -11,7 +11,6 @@
import javafx.scene.control.Tooltip;
import org.jabref.gui.DialogService;
-import org.jabref.gui.ai.chatting.chathistory.ChatHistoryService;
import org.jabref.gui.ai.components.aichat.AiChatGuardedComponent;
import org.jabref.gui.ai.components.privacynotice.PrivacyNoticeComponent;
import org.jabref.gui.ai.components.util.errorstate.ErrorStateComponent;
@@ -31,7 +30,6 @@
public class AiChatTab extends EntryEditorTab {
private final BibDatabaseContext bibDatabaseContext;
private final AiService aiService;
- private final ChatHistoryService chatHistoryService;
private final DialogService dialogService;
private final AiPreferences aiPreferences;
private final ExternalApplicationsPreferences externalApplicationsPreferences;
@@ -43,7 +41,6 @@ public class AiChatTab extends EntryEditorTab {
public AiChatTab(BibDatabaseContext bibDatabaseContext,
AiService aiService,
- ChatHistoryService chatHistoryService,
DialogService dialogService,
GuiPreferences preferences,
TaskExecutor taskExecutor
@@ -51,7 +48,6 @@ public AiChatTab(BibDatabaseContext bibDatabaseContext,
this.bibDatabaseContext = bibDatabaseContext;
this.aiService = aiService;
- this.chatHistoryService = chatHistoryService;
this.dialogService = dialogService;
this.aiPreferences = preferences.getAiPreferences();
@@ -76,7 +72,7 @@ public boolean shouldShow(BibEntry entry) {
*/
@Override
protected void bindToEntry(BibEntry entry) {
- previousBibEntry.ifPresent(previousBibEntry -> chatHistoryService.closeChatHistoryForEntry(previousBibEntry));
+ previousBibEntry.ifPresent(previousBibEntry -> aiService.getChatHistoryService().closeChatHistoryForEntry(previousBibEntry));
previousBibEntry = Optional.of(entry);
if (!aiPreferences.getEnableAi()) {
@@ -135,7 +131,7 @@ private void bindToCorrectEntry(BibEntry entry) {
setContent(new AiChatGuardedComponent(
chatName,
- chatHistoryService.getChatHistoryForEntry(entry),
+ aiService.getChatHistoryService().getChatHistoryForEntry(bibDatabaseContext, entry),
bibDatabaseContext,
FXCollections.observableArrayList(new ArrayList<>(List.of(entry))),
aiService,
diff --git a/src/main/java/org/jabref/gui/entryeditor/EntryEditor.java b/src/main/java/org/jabref/gui/entryeditor/EntryEditor.java
index 88c282366f2..6a382a7a59e 100644
--- a/src/main/java/org/jabref/gui/entryeditor/EntryEditor.java
+++ b/src/main/java/org/jabref/gui/entryeditor/EntryEditor.java
@@ -30,7 +30,6 @@
import org.jabref.gui.DialogService;
import org.jabref.gui.LibraryTab;
import org.jabref.gui.StateManager;
-import org.jabref.gui.ai.chatting.chathistory.ChatHistoryService;
import org.jabref.gui.citationkeypattern.GenerateCitationKeySingleAction;
import org.jabref.gui.cleanup.CleanupSingleAction;
import org.jabref.gui.entryeditor.citationrelationtab.CitationRelationsTab;
@@ -119,7 +118,6 @@ public class EntryEditor extends BorderPane {
@Inject private KeyBindingRepository keyBindingRepository;
@Inject private JournalAbbreviationRepository journalAbbreviationRepository;
@Inject private AiService aiService;
- @Inject private ChatHistoryService chatHistoryService;
private final List allPossibleTabs;
private final Collection previewTabs;
@@ -315,7 +313,7 @@ private List createTabs() {
tabs.add(new LatexCitationsTab(databaseContext, preferences, dialogService, directoryMonitorManager));
tabs.add(new FulltextSearchResultsTab(stateManager, preferences, dialogService, databaseContext, taskExecutor, libraryTab.searchQueryProperty()));
tabs.add(new AiSummaryTab(libraryTab.getBibDatabaseContext(), aiService, dialogService, preferences));
- tabs.add(new AiChatTab(libraryTab.getBibDatabaseContext(), aiService, chatHistoryService, dialogService, preferences, taskExecutor));
+ tabs.add(new AiChatTab(libraryTab.getBibDatabaseContext(), aiService, dialogService, preferences, taskExecutor));
return tabs;
}
diff --git a/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/CitationRelationsTab.java b/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/CitationRelationsTab.java
index 3ab2a167b9c..a550a0098a7 100644
--- a/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/CitationRelationsTab.java
+++ b/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/CitationRelationsTab.java
@@ -5,6 +5,7 @@
import java.net.URI;
import java.util.Arrays;
import java.util.List;
+import java.util.Optional;
import javax.swing.undo.UndoManager;
@@ -39,7 +40,12 @@
import org.jabref.gui.entryeditor.citationrelationtab.semanticscholar.CitationFetcher;
import org.jabref.gui.entryeditor.citationrelationtab.semanticscholar.SemanticScholarFetcher;
import org.jabref.gui.icon.IconTheme;
+import org.jabref.gui.mergeentries.EntriesMergeResult;
+import org.jabref.gui.mergeentries.MergeEntriesDialog;
import org.jabref.gui.preferences.GuiPreferences;
+import org.jabref.gui.undo.NamedCompound;
+import org.jabref.gui.undo.UndoableInsertEntries;
+import org.jabref.gui.undo.UndoableRemoveEntries;
import org.jabref.gui.util.NoSelectionModel;
import org.jabref.gui.util.ViewModelListCellFactory;
import org.jabref.logic.bibtex.BibEntryWriter;
@@ -51,6 +57,7 @@
import org.jabref.logic.os.OS;
import org.jabref.logic.util.BackgroundTask;
import org.jabref.logic.util.TaskExecutor;
+import org.jabref.model.database.BibDatabase;
import org.jabref.model.database.BibDatabaseContext;
import org.jabref.model.database.BibDatabaseMode;
import org.jabref.model.database.BibDatabaseModeDetection;
@@ -89,6 +96,8 @@ public class CitationRelationsTab extends EntryEditorTab {
private final CitationsRelationsTabViewModel citationsRelationsTabViewModel;
private final DuplicateCheck duplicateCheck;
private final BibEntryTypesManager entryTypesManager;
+ private final StateManager stateManager;
+ private final UndoManager undoManager;
public CitationRelationsTab(DialogService dialogService,
BibDatabaseContext databaseContext,
@@ -104,6 +113,8 @@ public CitationRelationsTab(DialogService dialogService,
this.preferences = preferences;
this.libraryTab = libraryTab;
this.taskExecutor = taskExecutor;
+ this.undoManager = undoManager;
+ this.stateManager = stateManager;
setText(Localization.lang("Citation relations"));
setTooltip(new Tooltip(Localization.lang("Show articles related by citation")));
@@ -231,13 +242,20 @@ private void styleFetchedListView(CheckListView listView)
Button jumpTo = IconTheme.JabRefIcons.LINK.asButton();
jumpTo.setTooltip(new Tooltip(Localization.lang("Jump to entry in library")));
jumpTo.getStyleClass().add("addEntryButton");
- jumpTo.setOnMouseClicked(event -> {
- citingTask.cancel();
- citedByTask.cancel();
- libraryTab.showAndEdit(entry.localEntry());
- libraryTab.clearAndSelect(entry.localEntry());
+ jumpTo.setOnMouseClicked(event -> jumpToEntry(entry));
+ hContainer.setOnMouseClicked(event -> {
+ if (event.getClickCount() == 2) {
+ jumpToEntry(entry);
+ }
});
vContainer.getChildren().add(jumpTo);
+
+ Button compareButton = IconTheme.JabRefIcons.MERGE_ENTRIES.asButton();
+ compareButton.setTooltip(new Tooltip(Localization.lang("Compare with existing entry")));
+ compareButton.setOnMouseClicked(event -> {
+ openPossibleDuplicateEntriesWindow(entry, listView);
+ });
+ vContainer.getChildren().add(compareButton);
} else {
ToggleButton addToggle = IconTheme.JabRefIcons.ADD.asToggleButton();
addToggle.setTooltip(new Tooltip(Localization.lang("Select entry")));
@@ -295,6 +313,13 @@ private void styleFetchedListView(CheckListView listView)
listView.setSelectionModel(new NoSelectionModel<>());
}
+ private void jumpToEntry(CitationRelationItem entry) {
+ citingTask.cancel();
+ citedByTask.cancel();
+ libraryTab.showAndEdit(entry.localEntry());
+ libraryTab.clearAndSelect(entry.localEntry());
+ }
+
/**
* @implNote This code is similar to {@link PreviewWithSourceTab#getSourceString(BibEntry, BibDatabaseMode, FieldPreferences, BibEntryTypesManager)}.
*/
@@ -504,4 +529,43 @@ private void importEntries(List entriesToImport, CitationF
dialogService.notify(Localization.lang("Number of entries successfully imported") + ": " + entriesToImport.size());
}
+
+ /**
+ * Function to open possible duplicate entries window to compare duplicate entries
+ *
+ * @param citationRelationItem duplicate in the citation relations tab
+ * @param listView CheckListView to display citations
+ */
+ private void openPossibleDuplicateEntriesWindow(CitationRelationItem citationRelationItem, CheckListView listView) {
+ BibEntry libraryEntry = citationRelationItem.localEntry();
+ BibEntry citationEntry = citationRelationItem.entry();
+ String leftHeader = Localization.lang("Library Entry");
+ String rightHeader = Localization.lang("Citation Entry");
+
+ MergeEntriesDialog dialog = new MergeEntriesDialog(libraryEntry, citationEntry, leftHeader, rightHeader, preferences);
+ dialog.setTitle(Localization.lang("Possible duplicate entries"));
+
+ Optional entriesMergeResult = dialogService.showCustomDialogAndWait(dialog);
+ entriesMergeResult.ifPresentOrElse(mergeResult -> {
+
+ BibEntry mergedEntry = mergeResult.mergedEntry();
+ // update local entry of selected citation relation item
+ listView.getItems().set(listView.getItems().indexOf(citationRelationItem), new CitationRelationItem(citationRelationItem.entry(), mergedEntry, true));
+
+ // Merge method is similar to MergeTwoEntriesAction#execute
+ BibDatabase database = stateManager.getActiveDatabase().get().getDatabase();
+ database.removeEntry(mergeResult.originalLeftEntry());
+ libraryTab.getMainTable().setCitationMergeMode(true);
+ database.insertEntry(mergedEntry);
+
+ NamedCompound ce = new NamedCompound(Localization.lang("Merge entries"));
+ ce.addEdit(new UndoableRemoveEntries(database, mergeResult.originalLeftEntry()));
+ ce.addEdit(new UndoableInsertEntries(stateManager.getActiveDatabase().get().getDatabase(), mergedEntry));
+ ce.end();
+
+ undoManager.addEdit(ce);
+
+ dialogService.notify(Localization.lang("Merged entries"));
+ }, () -> dialogService.notify(Localization.lang("Canceled merging entries")));
+ }
}
diff --git a/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/CitationsRelationsTabViewModel.java b/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/CitationsRelationsTabViewModel.java
index 7ae52965b8f..630ee2e387e 100644
--- a/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/CitationsRelationsTabViewModel.java
+++ b/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/CitationsRelationsTabViewModel.java
@@ -64,6 +64,7 @@ private void importCites(List entries, BibEntry existingEntry, ImportH
List citeKeys = getExistingEntriesFromCiteField(existingEntry);
citeKeys.removeIf(String::isEmpty);
+
for (BibEntry entryToCite : entries) {
if (generateNewKeyOnImport || entryToCite.getCitationKey().isEmpty()) {
String key = generator.generateKey(entryToCite);
diff --git a/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/semanticscholar/SemanticScholarFetcher.java b/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/semanticscholar/SemanticScholarFetcher.java
index 557a135741e..a183f7082e4 100644
--- a/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/semanticscholar/SemanticScholarFetcher.java
+++ b/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/semanticscholar/SemanticScholarFetcher.java
@@ -9,15 +9,14 @@
import org.jabref.logic.importer.ImporterPreferences;
import org.jabref.logic.importer.fetcher.CustomizableKeyFetcher;
import org.jabref.logic.net.URLDownload;
-import org.jabref.logic.util.BuildInfo;
import org.jabref.model.entry.BibEntry;
import com.google.gson.Gson;
public class SemanticScholarFetcher implements CitationFetcher, CustomizableKeyFetcher {
- private static final String SEMANTIC_SCHOLAR_API = "https://api.semanticscholar.org/graph/v1/";
+ public static final String FETCHER_NAME = "Semantic Scholar Citations Fetcher";
- private static final String API_KEY = new BuildInfo().semanticScholarApiKey;
+ private static final String SEMANTIC_SCHOLAR_API = "https://api.semanticscholar.org/graph/v1/";
private final ImporterPreferences importerPreferences;
@@ -45,10 +44,8 @@ public List searchCitedBy(BibEntry entry) throws FetcherException {
}
URLDownload urlDownload = new URLDownload(citationsUrl);
- String apiKey = getApiKey();
- if (!apiKey.isEmpty()) {
- urlDownload.addHeader("x-api-key", apiKey);
- }
+ importerPreferences.getApiKey(getName()).ifPresent(apiKey -> urlDownload.addHeader("x-api-key", apiKey));
+
CitationsResponse citationsResponse = new Gson()
.fromJson(urlDownload.asString(), CitationsResponse.class);
@@ -71,10 +68,7 @@ public List searchCiting(BibEntry entry) throws FetcherException {
}
URLDownload urlDownload = new URLDownload(referencesUrl);
- String apiKey = getApiKey();
- if (!apiKey.isEmpty()) {
- urlDownload.addHeader("x-api-key", apiKey);
- }
+ importerPreferences.getApiKey(getName()).ifPresent(apiKey -> urlDownload.addHeader("x-api-key", apiKey));
ReferencesResponse referencesResponse = new Gson()
.fromJson(urlDownload.asString(), ReferencesResponse.class);
@@ -86,10 +80,6 @@ public List searchCiting(BibEntry entry) throws FetcherException {
@Override
public String getName() {
- return "Semantic Scholar Citations Fetcher";
- }
-
- private String getApiKey() {
- return importerPreferences.getApiKey(getName()).orElse(API_KEY);
+ return FETCHER_NAME;
}
}
diff --git a/src/main/java/org/jabref/gui/exporter/ExportCommand.java b/src/main/java/org/jabref/gui/exporter/ExportCommand.java
index 4cadba61f5b..5cf0024cb9b 100644
--- a/src/main/java/org/jabref/gui/exporter/ExportCommand.java
+++ b/src/main/java/org/jabref/gui/exporter/ExportCommand.java
@@ -79,9 +79,7 @@ public ExportCommand(ExportMethod exportMethod,
@Override
public void execute() {
// Get list of exporters and sort before adding to file dialog
- ExporterFactory exporterFactory = ExporterFactory.create(
- preferences,
- entryTypesManager);
+ ExporterFactory exporterFactory = ExporterFactory.create(preferences);
List exporters = exporterFactory.getExporters().stream()
.sorted(Comparator.comparing(Exporter::getName))
.collect(Collectors.toList());
diff --git a/src/main/java/org/jabref/gui/exporter/ExportToClipboardAction.java b/src/main/java/org/jabref/gui/exporter/ExportToClipboardAction.java
index 5fb8cb1fe0c..57551c8298a 100644
--- a/src/main/java/org/jabref/gui/exporter/ExportToClipboardAction.java
+++ b/src/main/java/org/jabref/gui/exporter/ExportToClipboardAction.java
@@ -27,7 +27,6 @@
import org.jabref.logic.util.StandardFileType;
import org.jabref.logic.util.TaskExecutor;
import org.jabref.model.entry.BibEntry;
-import org.jabref.model.entry.BibEntryTypesManager;
import com.airhacks.afterburner.injection.Injector;
import org.slf4j.Logger;
@@ -75,9 +74,7 @@ public void execute() {
return;
}
- ExporterFactory exporterFactory = ExporterFactory.create(
- preferences,
- Injector.instantiateModelOrService(BibEntryTypesManager.class));
+ ExporterFactory exporterFactory = ExporterFactory.create(preferences);
List exporters = exporterFactory.getExporters().stream()
.sorted(Comparator.comparing(Exporter::getName))
.filter(exporter -> SUPPORTED_FILETYPES.contains(exporter.getFileType()))
diff --git a/src/main/java/org/jabref/gui/exporter/SaveAllAction.java b/src/main/java/org/jabref/gui/exporter/SaveAllAction.java
index a31329039be..1de83c4a039 100644
--- a/src/main/java/org/jabref/gui/exporter/SaveAllAction.java
+++ b/src/main/java/org/jabref/gui/exporter/SaveAllAction.java
@@ -5,6 +5,7 @@
import org.jabref.gui.DialogService;
import org.jabref.gui.LibraryTab;
+import org.jabref.gui.StateManager;
import org.jabref.gui.actions.SimpleCommand;
import org.jabref.gui.preferences.GuiPreferences;
import org.jabref.logic.l10n.Localization;
@@ -12,16 +13,19 @@
import com.airhacks.afterburner.injection.Injector;
+import static org.jabref.gui.actions.ActionHelper.needsDatabase;
+
public class SaveAllAction extends SimpleCommand {
private final Supplier> tabsSupplier;
private final DialogService dialogService;
private final GuiPreferences preferences;
- public SaveAllAction(Supplier> tabsSupplier, GuiPreferences preferences, DialogService dialogService) {
+ public SaveAllAction(Supplier> tabsSupplier, GuiPreferences preferences, DialogService dialogService, StateManager stateManager) {
this.tabsSupplier = tabsSupplier;
this.dialogService = dialogService;
this.preferences = preferences;
+ this.executable.bind(needsDatabase(stateManager));
}
@Override
diff --git a/src/main/java/org/jabref/gui/externalfiles/ImportHandler.java b/src/main/java/org/jabref/gui/externalfiles/ImportHandler.java
index 669aa9a21bc..8e89ab5026f 100644
--- a/src/main/java/org/jabref/gui/externalfiles/ImportHandler.java
+++ b/src/main/java/org/jabref/gui/externalfiles/ImportHandler.java
@@ -194,6 +194,7 @@ public void importEntries(List entries) {
}
public void importCleanedEntries(List entries) {
+ entries = entries.stream().map(entry -> (BibEntry) entry.clone()).toList();
bibDatabaseContext.getDatabase().insertEntries(entries);
generateKeys(entries);
setAutomaticFields(entries);
diff --git a/src/main/java/org/jabref/gui/externalfiletype/ExternalFileType.java b/src/main/java/org/jabref/gui/externalfiletype/ExternalFileType.java
index 0a5a44259cf..fff38ae4daa 100644
--- a/src/main/java/org/jabref/gui/externalfiletype/ExternalFileType.java
+++ b/src/main/java/org/jabref/gui/externalfiletype/ExternalFileType.java
@@ -4,6 +4,9 @@
import org.jabref.model.entry.field.Field;
import org.jabref.model.entry.field.FieldFactory;
+/**
+ * "Twin" interface: {@link org.jabref.logic.util.FileType}
+ */
public interface ExternalFileType {
String getName();
diff --git a/src/main/java/org/jabref/gui/fieldeditors/EditorTextArea.java b/src/main/java/org/jabref/gui/fieldeditors/EditorTextArea.java
index 7b478f94695..fc18e591103 100644
--- a/src/main/java/org/jabref/gui/fieldeditors/EditorTextArea.java
+++ b/src/main/java/org/jabref/gui/fieldeditors/EditorTextArea.java
@@ -21,7 +21,7 @@ public class EditorTextArea extends TextArea implements Initializable, ContextMe
/**
* Variable that contains user-defined behavior for paste action.
*/
- private PasteActionHandler pasteActionHandler = () -> {
+ private Runnable pasteActionHandler = () -> {
// Set empty paste behavior by default
};
@@ -57,7 +57,7 @@ public void initialize(URL location, ResourceBundle resources) {
*
* @param handler an instance of PasteActionHandler that describes paste behavior
*/
- public void setPasteActionHandler(PasteActionHandler handler) {
+ public void setPasteActionHandler(Runnable handler) {
Objects.requireNonNull(handler);
this.pasteActionHandler = handler;
}
@@ -68,14 +68,6 @@ public void setPasteActionHandler(PasteActionHandler handler) {
@Override
public void paste() {
super.paste();
- pasteActionHandler.handle();
- }
-
- /**
- * Interface presents user-described paste behaviour applying to paste method
- */
- @FunctionalInterface
- public interface PasteActionHandler {
- void handle();
+ pasteActionHandler.run();
}
}
diff --git a/src/main/java/org/jabref/gui/fieldeditors/EditorTextField.java b/src/main/java/org/jabref/gui/fieldeditors/EditorTextField.java
index 34fd2be09f4..08cd9dde886 100644
--- a/src/main/java/org/jabref/gui/fieldeditors/EditorTextField.java
+++ b/src/main/java/org/jabref/gui/fieldeditors/EditorTextField.java
@@ -2,6 +2,7 @@
import java.net.URL;
import java.util.List;
+import java.util.Objects;
import java.util.ResourceBundle;
import java.util.function.Supplier;
@@ -19,6 +20,9 @@
public class EditorTextField extends TextField implements Initializable, ContextMenuAddable {
private final ContextMenu contextMenu = new ContextMenu();
+ private Runnable additionalPasteActionHandler = () -> {
+ // No additional paste behavior
+ };
public EditorTextField() {
this("");
@@ -47,4 +51,15 @@ public void initContextMenu(final Supplier> items, KeyBindingRepo
public void initialize(URL location, ResourceBundle resources) {
// not needed
}
+
+ public void setAdditionalPasteActionHandler(Runnable handler) {
+ Objects.requireNonNull(handler);
+ this.additionalPasteActionHandler = handler;
+ }
+
+ @Override
+ public void paste() {
+ super.paste();
+ additionalPasteActionHandler.run();
+ }
}
diff --git a/src/main/java/org/jabref/gui/fieldeditors/ISSNEditor.fxml b/src/main/java/org/jabref/gui/fieldeditors/ISSNEditor.fxml
index 44d70fb78c4..7358de4c2b1 100644
--- a/src/main/java/org/jabref/gui/fieldeditors/ISSNEditor.fxml
+++ b/src/main/java/org/jabref/gui/fieldeditors/ISSNEditor.fxml
@@ -3,11 +3,11 @@
-
+
-
+
diff --git a/src/main/java/org/jabref/gui/fieldeditors/ISSNEditor.java b/src/main/java/org/jabref/gui/fieldeditors/ISSNEditor.java
index e273faa16ce..d0dadbb3888 100644
--- a/src/main/java/org/jabref/gui/fieldeditors/ISSNEditor.java
+++ b/src/main/java/org/jabref/gui/fieldeditors/ISSNEditor.java
@@ -27,7 +27,7 @@
public class ISSNEditor extends HBox implements FieldEditorFX {
@FXML private ISSNEditorViewModel viewModel;
- @FXML private EditorTextArea textArea;
+ @FXML private EditorTextField textField;
@FXML private Button journalInfoButton;
@FXML private Button fetchInformationByIdentifierButton;
@@ -60,9 +60,9 @@ public ISSNEditor(Field field,
stateManager,
preferences);
- establishBinding(textArea, viewModel.textProperty(), keyBindingRepository, undoAction, redoAction);
- textArea.initContextMenu(new DefaultMenu(textArea), keyBindingRepository);
- new EditorValidator(preferences).configureValidation(viewModel.getFieldValidator().getValidationStatus(), textArea);
+ establishBinding(textField, viewModel.textProperty(), keyBindingRepository, undoAction, redoAction);
+ textField.initContextMenu(new DefaultMenu(textField), keyBindingRepository);
+ new EditorValidator(preferences).configureValidation(viewModel.getFieldValidator().getValidationStatus(), textField);
}
public ISSNEditorViewModel getViewModel() {
@@ -82,7 +82,7 @@ public Parent getNode() {
@Override
public void requestFocus() {
- textArea.requestFocus();
+ textField.requestFocus();
}
@FXML
diff --git a/src/main/java/org/jabref/gui/fieldeditors/LinkedFileViewModel.java b/src/main/java/org/jabref/gui/fieldeditors/LinkedFileViewModel.java
index 7d72252ada2..b1a407e40a1 100644
--- a/src/main/java/org/jabref/gui/fieldeditors/LinkedFileViewModel.java
+++ b/src/main/java/org/jabref/gui/fieldeditors/LinkedFileViewModel.java
@@ -40,7 +40,7 @@
import org.jabref.logic.importer.fileformat.PdfContentImporter;
import org.jabref.logic.importer.fileformat.PdfEmbeddedBibFileImporter;
import org.jabref.logic.importer.fileformat.PdfGrobidImporter;
-import org.jabref.logic.importer.fileformat.PdfVerbatimBibTextImporter;
+import org.jabref.logic.importer.fileformat.PdfVerbatimBibtexImporter;
import org.jabref.logic.importer.fileformat.PdfXmpImporter;
import org.jabref.logic.l10n.Localization;
import org.jabref.logic.util.TaskExecutor;
@@ -460,7 +460,7 @@ public void parsePdfMetadataAndShowMergeDialog() {
MultiMergeEntriesView dialog = new MultiMergeEntriesView(preferences, taskExecutor);
dialog.setTitle(Localization.lang("Merge PDF metadata"));
dialog.addSource(Localization.lang("Entry"), entry);
- dialog.addSource(Localization.lang("Verbatim"), wrapImporterToSupplier(new PdfVerbatimBibTextImporter(preferences.getImportFormatPreferences()), filePath));
+ dialog.addSource(Localization.lang("Verbatim"), wrapImporterToSupplier(new PdfVerbatimBibtexImporter(preferences.getImportFormatPreferences()), filePath));
dialog.addSource(Localization.lang("Embedded"), wrapImporterToSupplier(new PdfEmbeddedBibFileImporter(preferences.getImportFormatPreferences()), filePath));
if (preferences.getGrobidPreferences().isGrobidEnabled()) {
dialog.addSource("Grobid", wrapImporterToSupplier(new PdfGrobidImporter(preferences.getImportFormatPreferences()), filePath));
diff --git a/src/main/java/org/jabref/gui/fieldeditors/OwnerEditor.fxml b/src/main/java/org/jabref/gui/fieldeditors/OwnerEditor.fxml
index 87b4ead077c..ee178badcb9 100644
--- a/src/main/java/org/jabref/gui/fieldeditors/OwnerEditor.fxml
+++ b/src/main/java/org/jabref/gui/fieldeditors/OwnerEditor.fxml
@@ -3,11 +3,11 @@
-
+
-
+
diff --git a/src/main/java/org/jabref/gui/fieldeditors/OwnerEditor.java b/src/main/java/org/jabref/gui/fieldeditors/OwnerEditor.java
index 01927ce5289..823214d6d0f 100644
--- a/src/main/java/org/jabref/gui/fieldeditors/OwnerEditor.java
+++ b/src/main/java/org/jabref/gui/fieldeditors/OwnerEditor.java
@@ -22,7 +22,7 @@
public class OwnerEditor extends HBox implements FieldEditorFX {
@FXML private OwnerEditorViewModel viewModel;
- @FXML private EditorTextArea textArea;
+ @FXML private EditorTextField textField;
@Inject private GuiPreferences preferences;
@Inject private KeyBindingRepository keyBindingRepository;
@@ -38,9 +38,9 @@ public OwnerEditor(Field field,
.load();
this.viewModel = new OwnerEditorViewModel(field, suggestionProvider, preferences, fieldCheckers, undoManager);
- establishBinding(textArea, viewModel.textProperty(), keyBindingRepository, undoAction, redoAction);
- textArea.initContextMenu(EditorMenus.getNameMenu(textArea), keyBindingRepository);
- new EditorValidator(preferences).configureValidation(viewModel.getFieldValidator().getValidationStatus(), textArea);
+ establishBinding(textField, viewModel.textProperty(), keyBindingRepository, undoAction, redoAction);
+ textField.initContextMenu(EditorMenus.getNameMenu(textField), keyBindingRepository);
+ new EditorValidator(preferences).configureValidation(viewModel.getFieldValidator().getValidationStatus(), textField);
}
public OwnerEditorViewModel getViewModel() {
diff --git a/src/main/java/org/jabref/gui/fieldeditors/UrlEditor.fxml b/src/main/java/org/jabref/gui/fieldeditors/UrlEditor.fxml
index 34194ff6c48..99f8b179e7d 100644
--- a/src/main/java/org/jabref/gui/fieldeditors/UrlEditor.fxml
+++ b/src/main/java/org/jabref/gui/fieldeditors/UrlEditor.fxml
@@ -3,11 +3,11 @@
-
+
-
+
diff --git a/src/main/java/org/jabref/gui/fieldeditors/UrlEditor.java b/src/main/java/org/jabref/gui/fieldeditors/UrlEditor.java
index 0951f3921d2..b31a036b3da 100644
--- a/src/main/java/org/jabref/gui/fieldeditors/UrlEditor.java
+++ b/src/main/java/org/jabref/gui/fieldeditors/UrlEditor.java
@@ -30,7 +30,7 @@
public class UrlEditor extends HBox implements FieldEditorFX {
@FXML private final UrlEditorViewModel viewModel;
- @FXML private EditorTextArea textArea;
+ @FXML private EditorTextField textField;
@Inject private DialogService dialogService;
@Inject private GuiPreferences preferences;
@@ -48,15 +48,15 @@ public UrlEditor(Field field,
this.viewModel = new UrlEditorViewModel(field, suggestionProvider, dialogService, preferences, fieldCheckers, undoManager);
- establishBinding(textArea, viewModel.textProperty(), keyBindingRepository, undoAction, redoAction);
+ establishBinding(textField, viewModel.textProperty(), keyBindingRepository, undoAction, redoAction);
- Supplier> contextMenuSupplier = EditorMenus.getCleanupUrlMenu(textArea);
- textArea.initContextMenu(contextMenuSupplier, preferences.getKeyBindingRepository());
+ Supplier> contextMenuSupplier = EditorMenus.getCleanupUrlMenu(textField);
+ textField.initContextMenu(contextMenuSupplier, preferences.getKeyBindingRepository());
// init paste handler for UrlEditor to format pasted url link in textArea
- textArea.setPasteActionHandler(() -> textArea.setText(new CleanupUrlFormatter().format(new TrimWhitespaceFormatter().format(textArea.getText()))));
+ textField.setAdditionalPasteActionHandler(() -> textField.setText(new CleanupUrlFormatter().format(new TrimWhitespaceFormatter().format(textField.getText()))));
- new EditorValidator(preferences).configureValidation(viewModel.getFieldValidator().getValidationStatus(), textArea);
+ new EditorValidator(preferences).configureValidation(viewModel.getFieldValidator().getValidationStatus(), textField);
}
public UrlEditorViewModel getViewModel() {
diff --git a/src/main/java/org/jabref/gui/fieldeditors/contextmenu/EditorMenus.java b/src/main/java/org/jabref/gui/fieldeditors/contextmenu/EditorMenus.java
index 4c02920b76e..459166a3fe4 100644
--- a/src/main/java/org/jabref/gui/fieldeditors/contextmenu/EditorMenus.java
+++ b/src/main/java/org/jabref/gui/fieldeditors/contextmenu/EditorMenus.java
@@ -6,7 +6,7 @@
import javafx.scene.control.MenuItem;
import javafx.scene.control.SeparatorMenuItem;
-import javafx.scene.control.TextArea;
+import javafx.scene.control.TextField;
import javafx.scene.control.TextInputControl;
import org.jabref.gui.ClipBoardManager;
@@ -51,20 +51,20 @@ public static Supplier> getNameMenu(final TextInputControl textIn
/**
* The default context menu with a specific menu copying a DOI/ DOI URL.
*
- * @param textArea text-area that this menu will be connected to
+ * @param textField text-field that this menu will be connected to
* @return menu containing items of the default menu and an item for copying a DOI/DOI URL
*/
- public static Supplier> getDOIMenu(TextArea textArea, DialogService dialogService) {
+ public static Supplier> getDOIMenu(TextField textField, DialogService dialogService) {
return () -> {
ActionFactory factory = new ActionFactory();
ClipBoardManager clipBoardManager = Injector.instantiateModelOrService(ClipBoardManager.class);
- MenuItem copyDoiMenuItem = factory.createMenuItem(StandardActions.COPY_DOI, new CopyDoiUrlAction(textArea, StandardActions.COPY_DOI, dialogService, clipBoardManager));
- MenuItem copyDoiUrlMenuItem = factory.createMenuItem(StandardActions.COPY_DOI_URL, new CopyDoiUrlAction(textArea, StandardActions.COPY_DOI_URL, dialogService, clipBoardManager));
+ MenuItem copyDoiMenuItem = factory.createMenuItem(StandardActions.COPY_DOI, new CopyDoiUrlAction(textField, StandardActions.COPY_DOI, dialogService, clipBoardManager));
+ MenuItem copyDoiUrlMenuItem = factory.createMenuItem(StandardActions.COPY_DOI_URL, new CopyDoiUrlAction(textField, StandardActions.COPY_DOI_URL, dialogService, clipBoardManager));
List menuItems = new ArrayList<>();
menuItems.add(copyDoiMenuItem);
menuItems.add(copyDoiUrlMenuItem);
menuItems.add(new SeparatorMenuItem());
- menuItems.addAll(new DefaultMenu(textArea).get());
+ menuItems.addAll(new DefaultMenu(textField).get());
return menuItems;
};
}
@@ -72,14 +72,14 @@ public static Supplier> getDOIMenu(TextArea textArea, DialogServi
/**
* The default context menu with a specific menu item to cleanup URL.
*
- * @param textArea text-area that this menu will be connected to
+ * @param textField text field that this menu will be connected to
* @return menu containing items of the default menu and an item to cleanup a URL
*/
- public static Supplier> getCleanupUrlMenu(TextArea textArea) {
+ public static Supplier> getCleanupUrlMenu(TextField textField) {
return () -> {
MenuItem cleanupURL = new MenuItem(Localization.lang("Cleanup URL link"));
- cleanupURL.setDisable(textArea.textProperty().isEmpty().get());
- cleanupURL.setOnAction(event -> textArea.setText(new CleanupUrlFormatter().format(textArea.getText())));
+ cleanupURL.setDisable(textField.textProperty().isEmpty().get());
+ cleanupURL.setOnAction(event -> textField.setText(new CleanupUrlFormatter().format(textField.getText())));
List menuItems = new ArrayList<>();
menuItems.add(cleanupURL);
return menuItems;
diff --git a/src/main/java/org/jabref/gui/fieldeditors/identifier/IdentifierEditor.fxml b/src/main/java/org/jabref/gui/fieldeditors/identifier/IdentifierEditor.fxml
index c40db788aa6..9132d31d21a 100644
--- a/src/main/java/org/jabref/gui/fieldeditors/identifier/IdentifierEditor.fxml
+++ b/src/main/java/org/jabref/gui/fieldeditors/identifier/IdentifierEditor.fxml
@@ -5,11 +5,11 @@
-
+
-
+
diff --git a/src/main/java/org/jabref/gui/fieldeditors/identifier/IdentifierEditor.java b/src/main/java/org/jabref/gui/fieldeditors/identifier/IdentifierEditor.java
index f0c308f9179..105472c79be 100644
--- a/src/main/java/org/jabref/gui/fieldeditors/identifier/IdentifierEditor.java
+++ b/src/main/java/org/jabref/gui/fieldeditors/identifier/IdentifierEditor.java
@@ -13,7 +13,7 @@
import org.jabref.gui.DialogService;
import org.jabref.gui.StateManager;
import org.jabref.gui.autocompleter.SuggestionProvider;
-import org.jabref.gui.fieldeditors.EditorTextArea;
+import org.jabref.gui.fieldeditors.EditorTextField;
import org.jabref.gui.fieldeditors.EditorValidator;
import org.jabref.gui.fieldeditors.FieldEditorFX;
import org.jabref.gui.fieldeditors.contextmenu.DefaultMenu;
@@ -36,7 +36,7 @@
public class IdentifierEditor extends HBox implements FieldEditorFX {
@FXML private BaseIdentifierEditorViewModel> viewModel;
- @FXML private EditorTextArea textArea;
+ @FXML private EditorTextField textField;
@FXML private Button fetchInformationByIdentifierButton;
@FXML private Button lookupIdentifierButton;
@@ -74,7 +74,7 @@ public IdentifierEditor(Field field,
.root(this)
.load();
- textArea.textProperty().bindBidirectional(viewModel.textProperty());
+ textField.textProperty().bindBidirectional(viewModel.textProperty());
fetchInformationByIdentifierButton.setTooltip(
new Tooltip(Localization.lang("Get bibliographic data from %0", field.getDisplayName())));
@@ -82,12 +82,12 @@ public IdentifierEditor(Field field,
new Tooltip(Localization.lang("Look up %0", field.getDisplayName())));
if (field.equals(DOI)) {
- textArea.initContextMenu(EditorMenus.getDOIMenu(textArea, dialogService), preferences.getKeyBindingRepository());
+ textField.initContextMenu(EditorMenus.getDOIMenu(textField, dialogService), preferences.getKeyBindingRepository());
} else {
- textArea.initContextMenu(new DefaultMenu(textArea), preferences.getKeyBindingRepository());
+ textField.initContextMenu(new DefaultMenu(textField), preferences.getKeyBindingRepository());
}
- new EditorValidator(preferences).configureValidation(viewModel.getFieldValidator().getValidationStatus(), textArea);
+ new EditorValidator(preferences).configureValidation(viewModel.getFieldValidator().getValidationStatus(), textField);
}
public BaseIdentifierEditorViewModel> getViewModel() {
diff --git a/src/main/java/org/jabref/gui/frame/JabRefFrame.java b/src/main/java/org/jabref/gui/frame/JabRefFrame.java
index d38d511fe58..c40a946cffc 100644
--- a/src/main/java/org/jabref/gui/frame/JabRefFrame.java
+++ b/src/main/java/org/jabref/gui/frame/JabRefFrame.java
@@ -33,7 +33,6 @@
import org.jabref.gui.actions.ActionHelper;
import org.jabref.gui.actions.SimpleCommand;
import org.jabref.gui.actions.StandardActions;
-import org.jabref.gui.ai.chatting.chathistory.ChatHistoryService;
import org.jabref.gui.desktop.os.NativeDesktop;
import org.jabref.gui.importer.NewEntryAction;
import org.jabref.gui.importer.actions.OpenDatabaseAction;
@@ -77,7 +76,6 @@ public class JabRefFrame extends BorderPane implements LibraryTabContainer, UiMe
private final SplitPane splitPane = new SplitPane();
private final GuiPreferences preferences;
private final AiService aiService;
- private final ChatHistoryService chatHistoryService;
private final GlobalSearchBar globalSearchBar;
private final FileHistoryMenu fileHistory;
@@ -106,7 +104,6 @@ public JabRefFrame(Stage mainStage,
FileUpdateMonitor fileUpdateMonitor,
GuiPreferences preferences,
AiService aiService,
- ChatHistoryService chatHistoryService,
StateManager stateManager,
CountingUndoManager undoManager,
BibEntryTypesManager entryTypesManager,
@@ -117,7 +114,6 @@ public JabRefFrame(Stage mainStage,
this.fileUpdateMonitor = fileUpdateMonitor;
this.preferences = preferences;
this.aiService = aiService;
- this.chatHistoryService = chatHistoryService;
this.stateManager = stateManager;
this.undoManager = undoManager;
this.entryTypesManager = entryTypesManager;
@@ -157,10 +153,10 @@ public JabRefFrame(Stage mainStage,
this.sidePane = new SidePane(
this,
this.preferences,
- chatHistoryService,
Injector.instantiateModelOrService(JournalAbbreviationRepository.class),
taskExecutor,
dialogService,
+ aiService,
stateManager,
fileUpdateMonitor,
entryTypesManager,
@@ -437,6 +433,7 @@ public void addTab(@NonNull BibDatabaseContext databaseContext, boolean raisePan
databaseContext,
this,
dialogService,
+ aiService,
preferences,
stateManager,
fileUpdateMonitor,
diff --git a/src/main/java/org/jabref/gui/frame/MainMenu.java b/src/main/java/org/jabref/gui/frame/MainMenu.java
index 96ab72d70d4..4a90a556172 100644
--- a/src/main/java/org/jabref/gui/frame/MainMenu.java
+++ b/src/main/java/org/jabref/gui/frame/MainMenu.java
@@ -15,8 +15,6 @@
import org.jabref.gui.actions.StandardActions;
import org.jabref.gui.ai.ClearEmbeddingsAction;
import org.jabref.gui.auximport.NewSubLibraryAction;
-import org.jabref.gui.bibtexextractor.ExtractBibtexActionOffline;
-import org.jabref.gui.bibtexextractor.ExtractBibtexActionOnline;
import org.jabref.gui.citationkeypattern.GenerateCitationKeyAction;
import org.jabref.gui.cleanup.CleanupAction;
import org.jabref.gui.copyfiles.CopyFilesAction;
@@ -54,6 +52,7 @@
import org.jabref.gui.maintable.NewLibraryFromPdfActionOffline;
import org.jabref.gui.maintable.NewLibraryFromPdfActionOnline;
import org.jabref.gui.mergeentries.MergeEntriesAction;
+import org.jabref.gui.plaincitationparser.PlainCitationParserAction;
import org.jabref.gui.preferences.GuiPreferences;
import org.jabref.gui.preferences.ShowPreferencesAction;
import org.jabref.gui.preview.CopyCitationAction;
@@ -153,7 +152,7 @@ private void createMenu() {
fileHistoryMenu,
factory.createMenuItem(StandardActions.SAVE_LIBRARY, new SaveAction(SaveAction.SaveMethod.SAVE, frame::getCurrentLibraryTab, dialogService, preferences, stateManager)),
factory.createMenuItem(StandardActions.SAVE_LIBRARY_AS, new SaveAction(SaveAction.SaveMethod.SAVE_AS, frame::getCurrentLibraryTab, dialogService, preferences, stateManager)),
- factory.createMenuItem(StandardActions.SAVE_ALL, new SaveAllAction(frame::getLibraryTabs, preferences, dialogService)),
+ factory.createMenuItem(StandardActions.SAVE_ALL, new SaveAllAction(frame::getLibraryTabs, preferences, dialogService, stateManager)),
factory.createMenuItem(StandardActions.CLOSE_LIBRARY, new JabRefFrame.CloseDatabaseAction(frame, stateManager)),
new SeparatorMenuItem(),
@@ -232,11 +231,9 @@ private void createMenu() {
}
});
- MenuItem newEntryFromPlainTextOnline = factory.createMenuItem(StandardActions.NEW_ENTRY_FROM_PLAIN_TEXT_ONLINE, new ExtractBibtexActionOnline(dialogService, preferences, stateManager, true));
library.getItems().addAll(
factory.createMenuItem(StandardActions.NEW_ENTRY, new NewEntryAction(frame::getCurrentLibraryTab, dialogService, preferences, stateManager)),
- newEntryFromPlainTextOnline,
- factory.createMenuItem(StandardActions.NEW_ENTRY_FROM_PLAIN_TEXT_OFFLINE, new ExtractBibtexActionOffline(dialogService, stateManager)),
+ factory.createMenuItem(StandardActions.NEW_ENTRY_FROM_PLAIN_TEXT, new PlainCitationParserAction(dialogService, stateManager)),
factory.createMenuItem(StandardActions.DELETE_ENTRY, new EditAction(StandardActions.DELETE_ENTRY, frame::getCurrentLibraryTab, stateManager, undoManager)),
new SeparatorMenuItem(),
diff --git a/src/main/java/org/jabref/gui/frame/MainToolBar.java b/src/main/java/org/jabref/gui/frame/MainToolBar.java
index e07467e2c3d..bcbe2b8ba69 100644
--- a/src/main/java/org/jabref/gui/frame/MainToolBar.java
+++ b/src/main/java/org/jabref/gui/frame/MainToolBar.java
@@ -20,7 +20,6 @@
import org.jabref.gui.actions.ActionFactory;
import org.jabref.gui.actions.ActionHelper;
import org.jabref.gui.actions.StandardActions;
-import org.jabref.gui.bibtexextractor.ExtractBibtexActionOnline;
import org.jabref.gui.citationkeypattern.GenerateCitationKeyAction;
import org.jabref.gui.cleanup.CleanupAction;
import org.jabref.gui.edit.EditAction;
@@ -31,6 +30,7 @@
import org.jabref.gui.importer.NewDatabaseAction;
import org.jabref.gui.importer.NewEntryAction;
import org.jabref.gui.importer.actions.OpenDatabaseAction;
+import org.jabref.gui.plaincitationparser.PlainCitationParserAction;
import org.jabref.gui.preferences.GuiPreferences;
import org.jabref.gui.push.PushToApplicationCommand;
import org.jabref.gui.search.GlobalSearchBar;
@@ -107,10 +107,6 @@ private void createToolBar() {
// Setup Toolbar
- // The action itself asks the user if it is OK to use Grobid (in some cases).
- // Therefore, the condition of enablement is "only" if a library is opened. (Parameter "false")
- Button newEntryFromPlainTextOnlineButton = factory.createIconButton(StandardActions.NEW_ENTRY_FROM_PLAIN_TEXT, new ExtractBibtexActionOnline(dialogService, preferences, stateManager, false));
-
getItems().addAll(
new HBox(
factory.createIconButton(StandardActions.NEW_LIBRARY, new NewDatabaseAction(frame, preferences)),
@@ -127,7 +123,7 @@ private void createToolBar() {
factory.createIconButton(StandardActions.NEW_ARTICLE, new NewEntryAction(frame::getCurrentLibraryTab, StandardEntryType.Article, dialogService, preferences, stateManager)),
factory.createIconButton(StandardActions.NEW_ENTRY, new NewEntryAction(frame::getCurrentLibraryTab, dialogService, preferences, stateManager)),
createNewEntryFromIdButton(),
- newEntryFromPlainTextOnlineButton,
+ factory.createIconButton(StandardActions.NEW_ENTRY_FROM_PLAIN_TEXT, new PlainCitationParserAction(dialogService, stateManager)),
factory.createIconButton(StandardActions.DELETE_ENTRY, new EditAction(StandardActions.DELETE_ENTRY, frame::getCurrentLibraryTab, stateManager, undoManager))),
new Separator(Orientation.VERTICAL),
diff --git a/src/main/java/org/jabref/gui/groups/GroupDialogView.java b/src/main/java/org/jabref/gui/groups/GroupDialogView.java
index c1f537248ea..a355ca88fd0 100644
--- a/src/main/java/org/jabref/gui/groups/GroupDialogView.java
+++ b/src/main/java/org/jabref/gui/groups/GroupDialogView.java
@@ -265,7 +265,7 @@ private void openIconPicker() {
}
CustomTextField searchBox = new CustomTextField();
- searchBox.setPromptText(Localization.lang("Search") + "...");
+ searchBox.setPromptText(Localization.lang("Search..."));
searchBox.setLeft(IconTheme.JabRefIcons.SEARCH.getGraphicNode());
searchBox.textProperty().addListener((obs, oldValue, newValue) ->
filteredList.setPredicate(ikon -> newValue.isEmpty() || ikon.getDescription().toLowerCase()
diff --git a/src/main/java/org/jabref/gui/groups/GroupTreeView.java b/src/main/java/org/jabref/gui/groups/GroupTreeView.java
index a28919ef26e..7b6efa2ecdd 100644
--- a/src/main/java/org/jabref/gui/groups/GroupTreeView.java
+++ b/src/main/java/org/jabref/gui/groups/GroupTreeView.java
@@ -50,7 +50,6 @@
import org.jabref.gui.actions.ActionFactory;
import org.jabref.gui.actions.SimpleCommand;
import org.jabref.gui.actions.StandardActions;
-import org.jabref.gui.ai.chatting.chathistory.ChatHistoryService;
import org.jabref.gui.preferences.GuiPreferences;
import org.jabref.gui.search.SearchTextField;
import org.jabref.gui.util.BindingsHelper;
@@ -59,6 +58,7 @@
import org.jabref.gui.util.RecursiveTreeItem;
import org.jabref.gui.util.ViewModelTreeTableCellFactory;
import org.jabref.gui.util.ViewModelTreeTableRowFactory;
+import org.jabref.logic.ai.AiService;
import org.jabref.logic.l10n.Localization;
import org.jabref.logic.util.TaskExecutor;
import org.jabref.model.entry.BibEntry;
@@ -82,7 +82,7 @@ public class GroupTreeView extends BorderPane {
private final StateManager stateManager;
private final DialogService dialogService;
- private final ChatHistoryService chatHistoryService;
+ private final AiService aiService;
private final TaskExecutor taskExecutor;
private final GuiPreferences preferences;
@@ -108,13 +108,13 @@ public GroupTreeView(TaskExecutor taskExecutor,
StateManager stateManager,
GuiPreferences preferences,
DialogService dialogService,
- ChatHistoryService chatHistoryService
+ AiService aiService
) {
this.taskExecutor = taskExecutor;
this.stateManager = stateManager;
this.preferences = preferences;
this.dialogService = dialogService;
- this.chatHistoryService = chatHistoryService;
+ this.aiService = aiService;
createNodes();
this.getStylesheets().add(Objects.requireNonNull(GroupTreeView.class.getResource("GroupTree.css")).toExternalForm());
@@ -164,7 +164,7 @@ private void createNodes() {
private void initialize() {
this.localDragboard = stateManager.getLocalDragboard();
- viewModel = new GroupTreeViewModel(stateManager, dialogService, chatHistoryService, preferences, taskExecutor, localDragboard);
+ viewModel = new GroupTreeViewModel(stateManager, dialogService, aiService, preferences, taskExecutor, localDragboard);
// Set-up groups tree
groupTree.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
@@ -555,6 +555,8 @@ private ContextMenu createContextMenuForGroup(GroupNodeViewModel group) {
contextMenu.getItems().addAll(
factory.createMenuItem(StandardActions.GROUP_EDIT, new ContextAction(StandardActions.GROUP_EDIT, group)),
+ factory.createMenuItem(StandardActions.GROUP_GENERATE_EMBEDDINGS, new ContextAction(StandardActions.GROUP_GENERATE_EMBEDDINGS, group)),
+ factory.createMenuItem(StandardActions.GROUP_GENERATE_SUMMARIES, new ContextAction(StandardActions.GROUP_GENERATE_SUMMARIES, group)),
removeGroup,
new SeparatorMenuItem(),
factory.createMenuItem(StandardActions.GROUP_SUBGROUP_ADD, new ContextAction(StandardActions.GROUP_SUBGROUP_ADD, group)),
@@ -668,6 +670,10 @@ public void execute() {
viewModel.editGroup(group);
groupTree.refresh();
}
+ case GROUP_GENERATE_EMBEDDINGS ->
+ viewModel.generateEmbeddings(group);
+ case GROUP_GENERATE_SUMMARIES ->
+ viewModel.generateSummaries(group);
case GROUP_CHAT ->
viewModel.chatWithGroup(group);
case GROUP_SUBGROUP_ADD ->
diff --git a/src/main/java/org/jabref/gui/groups/GroupTreeViewModel.java b/src/main/java/org/jabref/gui/groups/GroupTreeViewModel.java
index 3046fa454d9..bdf00da96bc 100644
--- a/src/main/java/org/jabref/gui/groups/GroupTreeViewModel.java
+++ b/src/main/java/org/jabref/gui/groups/GroupTreeViewModel.java
@@ -24,7 +24,6 @@
import org.jabref.gui.AbstractViewModel;
import org.jabref.gui.DialogService;
import org.jabref.gui.StateManager;
-import org.jabref.gui.ai.chatting.chathistory.ChatHistoryService;
import org.jabref.gui.ai.components.aichat.AiChatWindow;
import org.jabref.gui.preferences.GuiPreferences;
import org.jabref.gui.util.CustomLocalDragboard;
@@ -33,6 +32,7 @@
import org.jabref.logic.util.TaskExecutor;
import org.jabref.model.database.BibDatabaseContext;
import org.jabref.model.entry.BibEntry;
+import org.jabref.model.entry.LinkedFile;
import org.jabref.model.groups.AbstractGroup;
import org.jabref.model.groups.AutomaticKeywordGroup;
import org.jabref.model.groups.AutomaticPersonsGroup;
@@ -44,7 +44,6 @@
import org.jabref.model.groups.WordKeywordGroup;
import org.jabref.model.metadata.MetaData;
-import com.airhacks.afterburner.injection.Injector;
import com.tobiasdiez.easybind.EasyBind;
import dev.langchain4j.data.message.ChatMessage;
@@ -54,7 +53,7 @@ public class GroupTreeViewModel extends AbstractViewModel {
private final ListProperty selectedGroups = new SimpleListProperty<>(FXCollections.observableArrayList());
private final StateManager stateManager;
private final DialogService dialogService;
- private final ChatHistoryService chatHistoryService;
+ private final AiService aiService;
private final GuiPreferences preferences;
private final TaskExecutor taskExecutor;
private final CustomLocalDragboard localDragboard;
@@ -78,10 +77,16 @@ public class GroupTreeViewModel extends AbstractViewModel {
};
private Optional currentDatabase = Optional.empty();
- public GroupTreeViewModel(StateManager stateManager, DialogService dialogService, ChatHistoryService chatHistoryService, GuiPreferences preferences, TaskExecutor taskExecutor, CustomLocalDragboard localDragboard) {
+ public GroupTreeViewModel(StateManager stateManager,
+ DialogService dialogService,
+ AiService aiService,
+ GuiPreferences preferences,
+ TaskExecutor taskExecutor,
+ CustomLocalDragboard localDragboard
+ ) {
this.stateManager = Objects.requireNonNull(stateManager);
this.dialogService = Objects.requireNonNull(dialogService);
- this.chatHistoryService = Objects.requireNonNull(chatHistoryService);
+ this.aiService = Objects.requireNonNull(aiService);
this.preferences = Objects.requireNonNull(preferences);
this.taskExecutor = Objects.requireNonNull(taskExecutor);
this.localDragboard = Objects.requireNonNull(localDragboard);
@@ -386,11 +391,7 @@ public void editGroup(GroupNodeViewModel oldGroup) {
}
public void chatWithGroup(GroupNodeViewModel group) {
- // This should probably be done some other way. Please don't blame, it's just a thing to make it quick and fast.
- if (currentDatabase.isEmpty()) {
- dialogService.showErrorDialogAndWait(Localization.lang("Unable to chat with group"), Localization.lang("No library is selected."));
- return;
- }
+ assert currentDatabase.isPresent();
StringProperty groupNameProperty = group.getGroupNode().getGroup().nameProperty();
@@ -399,7 +400,7 @@ public void chatWithGroup(GroupNodeViewModel group) {
StringProperty nameProperty = new SimpleStringProperty(Localization.lang("Group %0", groupNameProperty.get()));
groupNameProperty.addListener((obs, oldValue, newValue) -> nameProperty.setValue(Localization.lang("Group %0", groupNameProperty.get())));
- ObservableList chatHistory = chatHistoryService.getChatHistoryForGroup(group.getGroupNode());
+ ObservableList chatHistory = aiService.getChatHistoryService().getChatHistoryForGroup(currentDatabase.get(), group.getGroupNode());
ObservableList bibEntries = FXCollections.observableArrayList(group.getGroupNode().findMatches(currentDatabase.get().getDatabase()));
openAiChat(nameProperty, chatHistory, currentDatabase.get(), bibEntries);
@@ -412,7 +413,7 @@ private void openAiChat(StringProperty name, ObservableList chatHis
existingWindow.get().requestFocus();
} else {
AiChatWindow aiChatWindow = new AiChatWindow(
- Injector.instantiateModelOrService(AiService.class),
+ aiService,
dialogService,
preferences.getAiPreferences(),
preferences.getExternalApplicationsPreferences(),
@@ -430,6 +431,51 @@ private void openAiChat(StringProperty name, ObservableList chatHis
}
}
+ public void generateEmbeddings(GroupNodeViewModel groupNode) {
+ assert currentDatabase.isPresent();
+
+ AbstractGroup group = groupNode.getGroupNode().getGroup();
+
+ List linkedFiles = currentDatabase
+ .get()
+ .getDatabase()
+ .getEntries()
+ .stream()
+ .filter(group::isMatch)
+ .flatMap(entry -> entry.getFiles().stream())
+ .toList();
+
+ aiService.getIngestionService().ingest(
+ group.nameProperty(),
+ linkedFiles,
+ currentDatabase.get()
+ );
+
+ dialogService.notify(Localization.lang("Ingestion started for group \"%0\".", group.getName()));
+ }
+
+ public void generateSummaries(GroupNodeViewModel groupNode) {
+ assert currentDatabase.isPresent();
+
+ AbstractGroup group = groupNode.getGroupNode().getGroup();
+
+ List entries = currentDatabase
+ .get()
+ .getDatabase()
+ .getEntries()
+ .stream()
+ .filter(group::isMatch)
+ .toList();
+
+ aiService.getSummariesService().summarize(
+ group.nameProperty(),
+ entries,
+ currentDatabase.get()
+ );
+
+ dialogService.notify(Localization.lang("Summarization started for group \"%0\".", group.getName()));
+ }
+
public void removeSubgroups(GroupNodeViewModel group) {
boolean confirmation = dialogService.showConfirmationDialogAndWait(
Localization.lang("Remove subgroups"),
diff --git a/src/main/java/org/jabref/gui/importer/GrobidOptInDialogHelper.java b/src/main/java/org/jabref/gui/importer/GrobidOptInDialogHelper.java
index b049e104c32..6ef67bc17a0 100644
--- a/src/main/java/org/jabref/gui/importer/GrobidOptInDialogHelper.java
+++ b/src/main/java/org/jabref/gui/importer/GrobidOptInDialogHelper.java
@@ -1,7 +1,7 @@
package org.jabref.gui.importer;
import org.jabref.gui.DialogService;
-import org.jabref.logic.importer.fetcher.GrobidPreferences;
+import org.jabref.logic.importer.util.GrobidPreferences;
import org.jabref.logic.l10n.Localization;
/**
diff --git a/src/main/java/org/jabref/gui/importer/actions/OpenDatabaseAction.java b/src/main/java/org/jabref/gui/importer/actions/OpenDatabaseAction.java
index d30c7d79a3d..605e389e76a 100644
--- a/src/main/java/org/jabref/gui/importer/actions/OpenDatabaseAction.java
+++ b/src/main/java/org/jabref/gui/importer/actions/OpenDatabaseAction.java
@@ -200,6 +200,7 @@ private void openTheFile(Path file) {
backgroundTask,
file,
dialogService,
+ aiService,
preferences,
stateManager,
tabContainer,
diff --git a/src/main/java/org/jabref/gui/maintable/MainTable.java b/src/main/java/org/jabref/gui/maintable/MainTable.java
index 4a94fd56b5f..70285478536 100644
--- a/src/main/java/org/jabref/gui/maintable/MainTable.java
+++ b/src/main/java/org/jabref/gui/maintable/MainTable.java
@@ -82,6 +82,7 @@ public class MainTable extends TableView {
private long lastKeyPressTime;
private String columnSearchTerm;
+ private boolean citationMergeMode = false;
public MainTable(MainTableDataModel model,
LibraryTab libraryTab,
@@ -249,11 +250,18 @@ public void listen(EntriesAddedEvent event) {
}
public void clearAndSelect(BibEntry bibEntry) {
- getSelectionModel().clearSelection();
- findEntry(bibEntry).ifPresent(entry -> {
- getSelectionModel().select(entry);
- scrollTo(entry);
- });
+ // check if entries merged from citation relations tab
+ if (citationMergeMode) {
+ // keep original entry selected and reset citation merge mode
+ this.citationMergeMode = false;
+ } else {
+ // select new entry
+ getSelectionModel().clearSelection();
+ findEntry(bibEntry).ifPresent(entry -> {
+ getSelectionModel().select(entry);
+ scrollTo(entry);
+ });
+ }
}
private void scrollToNextMatchCategory() {
@@ -492,4 +500,8 @@ private Optional findEntry(BibEntry entry) {
.filter(viewModel -> viewModel.getEntry().equals(entry))
.findFirst();
}
+
+ public void setCitationMergeMode(boolean citationMerge) {
+ this.citationMergeMode = citationMerge;
+ }
}
diff --git a/src/main/java/org/jabref/gui/mergeentries/MergeEntriesDialog.java b/src/main/java/org/jabref/gui/mergeentries/MergeEntriesDialog.java
index 9613b457991..dc485757be0 100644
--- a/src/main/java/org/jabref/gui/mergeentries/MergeEntriesDialog.java
+++ b/src/main/java/org/jabref/gui/mergeentries/MergeEntriesDialog.java
@@ -23,6 +23,14 @@ public MergeEntriesDialog(BibEntry one, BibEntry two, GuiPreferences preferences
init();
}
+ public MergeEntriesDialog(BibEntry one, BibEntry two, String leftHeader, String rightHeader, GuiPreferences preferences) {
+ threeWayMergeView = new ThreeWayMergeView(one, two, leftHeader, rightHeader, preferences);
+ this.one = one;
+ this.two = two;
+
+ init();
+ }
+
/**
* Sets up the dialog
*/
diff --git a/src/main/java/org/jabref/gui/openoffice/OpenOfficePanel.java b/src/main/java/org/jabref/gui/openoffice/OpenOfficePanel.java
index 267526d0ad7..ad47cc921df 100644
--- a/src/main/java/org/jabref/gui/openoffice/OpenOfficePanel.java
+++ b/src/main/java/org/jabref/gui/openoffice/OpenOfficePanel.java
@@ -42,6 +42,7 @@
import org.jabref.gui.undo.UndoableKeyChange;
import org.jabref.gui.util.DirectoryDialogConfiguration;
import org.jabref.gui.util.UiTaskExecutor;
+import org.jabref.logic.ai.AiService;
import org.jabref.logic.citationkeypattern.CitationKeyGenerator;
import org.jabref.logic.citationkeypattern.CitationKeyPatternPreferences;
import org.jabref.logic.citationstyle.CitationStyle;
@@ -103,6 +104,7 @@ public class OpenOfficePanel {
private final ClipBoardManager clipBoardManager;
private final UndoManager undoManager;
private final UiTaskExecutor taskExecutor;
+ private final AiService aiService;
private final StyleLoader loader;
private final LibraryTabContainer tabContainer;
private final FileUpdateMonitor fileUpdateMonitor;
@@ -121,6 +123,7 @@ public OpenOfficePanel(LibraryTabContainer tabContainer,
JournalAbbreviationRepository abbreviationRepository,
UiTaskExecutor taskExecutor,
DialogService dialogService,
+ AiService aiService,
StateManager stateManager,
FileUpdateMonitor fileUpdateMonitor,
BibEntryTypesManager entryTypesManager,
@@ -134,6 +137,7 @@ public OpenOfficePanel(LibraryTabContainer tabContainer,
this.citationKeyPatternPreferences = citationKeyPatternPreferences;
this.taskExecutor = taskExecutor;
this.dialogService = dialogService;
+ this.aiService = aiService;
this.stateManager = stateManager;
this.clipBoardManager = clipBoardManager;
this.undoManager = undoManager;
@@ -322,6 +326,7 @@ private void exportEntries() {
databaseContext,
tabContainer,
dialogService,
+ aiService,
preferences,
stateManager,
fileUpdateMonitor,
diff --git a/src/main/java/org/jabref/gui/bibtexextractor/ExtractBibtexActionOffline.java b/src/main/java/org/jabref/gui/plaincitationparser/PlainCitationParserAction.java
similarity index 58%
rename from src/main/java/org/jabref/gui/bibtexextractor/ExtractBibtexActionOffline.java
rename to src/main/java/org/jabref/gui/plaincitationparser/PlainCitationParserAction.java
index ddd1eba3dda..16f63a8f92e 100644
--- a/src/main/java/org/jabref/gui/bibtexextractor/ExtractBibtexActionOffline.java
+++ b/src/main/java/org/jabref/gui/plaincitationparser/PlainCitationParserAction.java
@@ -1,4 +1,4 @@
-package org.jabref.gui.bibtexextractor;
+package org.jabref.gui.plaincitationparser;
import org.jabref.gui.DialogService;
import org.jabref.gui.StateManager;
@@ -6,17 +6,16 @@
import static org.jabref.gui.actions.ActionHelper.needsDatabase;
-public class ExtractBibtexActionOffline extends SimpleCommand {
-
+public class PlainCitationParserAction extends SimpleCommand {
private final DialogService dialogService;
- public ExtractBibtexActionOffline(DialogService dialogService, StateManager stateManager) {
+ public PlainCitationParserAction(DialogService dialogService, StateManager stateManager) {
this.dialogService = dialogService;
this.executable.bind(needsDatabase(stateManager));
}
@Override
public void execute() {
- dialogService.showCustomDialogAndWait(new ExtractBibtexDialog(false));
+ dialogService.showCustomDialogAndWait(new PlainCitationParserDialog());
}
}
diff --git a/src/main/java/org/jabref/gui/plaincitationparser/PlainCitationParserDialog.fxml b/src/main/java/org/jabref/gui/plaincitationparser/PlainCitationParserDialog.fxml
new file mode 100644
index 00000000000..46265fba049
--- /dev/null
+++ b/src/main/java/org/jabref/gui/plaincitationparser/PlainCitationParserDialog.fxml
@@ -0,0 +1,69 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/java/org/jabref/gui/bibtexextractor/ExtractBibtexDialog.java b/src/main/java/org/jabref/gui/plaincitationparser/PlainCitationParserDialog.java
similarity index 68%
rename from src/main/java/org/jabref/gui/bibtexextractor/ExtractBibtexDialog.java
rename to src/main/java/org/jabref/gui/plaincitationparser/PlainCitationParserDialog.java
index bf6266630b4..a72b8c66d3a 100644
--- a/src/main/java/org/jabref/gui/bibtexextractor/ExtractBibtexDialog.java
+++ b/src/main/java/org/jabref/gui/plaincitationparser/PlainCitationParserDialog.java
@@ -1,4 +1,4 @@
-package org.jabref.gui.bibtexextractor;
+package org.jabref.gui.plaincitationparser;
import javax.swing.undo.UndoManager;
@@ -6,6 +6,7 @@
import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonType;
+import javafx.scene.control.ComboBox;
import javafx.scene.control.TextArea;
import javafx.scene.control.Tooltip;
@@ -14,6 +15,9 @@
import org.jabref.gui.StateManager;
import org.jabref.gui.preferences.GuiPreferences;
import org.jabref.gui.util.BaseDialog;
+import org.jabref.gui.util.ViewModelListCellFactory;
+import org.jabref.logic.ai.AiService;
+import org.jabref.logic.importer.plaincitation.PlainCitationParserChoice;
import org.jabref.logic.l10n.Localization;
import org.jabref.logic.util.TaskExecutor;
import org.jabref.model.database.BibDatabaseContext;
@@ -30,49 +34,52 @@
* @implNote Instead of using inheritance, we do if/else checks.
*
*/
-public class ExtractBibtexDialog extends BaseDialog {
-
+public class PlainCitationParserDialog extends BaseDialog {
@Inject protected StateManager stateManager;
@Inject protected DialogService dialogService;
+ @Inject protected AiService aiService;
@Inject protected FileUpdateMonitor fileUpdateMonitor;
@Inject protected TaskExecutor taskExecutor;
@Inject protected UndoManager undoManager;
@Inject protected GuiPreferences preferences;
@FXML protected TextArea input;
- private final boolean onlineMode;
-
- @FXML private ButtonType parseButtonType;
+ @FXML protected ButtonType parseButtonType;
+ @FXML protected ComboBox parserChoice;
- public ExtractBibtexDialog(boolean onlineMode) {
- this.onlineMode = onlineMode;
+ public PlainCitationParserDialog() {
ViewLoader.view(this)
.load()
.setAsDialogPane(this);
- if (onlineMode) {
- this.setTitle(Localization.lang("Plain References Parser (online)"));
- } else {
- this.setTitle(Localization.lang("Plain References Parser (offline)"));
- }
+
+ this.setTitle(Localization.lang("Plain Citations Parser"));
}
@FXML
private void initialize() {
BibDatabaseContext database = stateManager.getActiveDatabase().orElseThrow(() -> new NullPointerException("Database null"));
- BibtexExtractorViewModel viewModel = new BibtexExtractorViewModel(
- onlineMode,
+
+ PlainCitationParserViewModel viewModel = new PlainCitationParserViewModel(
database,
dialogService,
+ aiService,
preferences,
fileUpdateMonitor,
taskExecutor,
undoManager,
stateManager);
+ new ViewModelListCellFactory()
+ .withText(PlainCitationParserChoice::getLocalizedName)
+ .install(parserChoice);
+ parserChoice.getItems().setAll(viewModel.plainCitationParsers());
+ parserChoice.valueProperty().bindBidirectional(viewModel.parserChoice());
+
input.textProperty().bindBidirectional(viewModel.inputTextProperty());
+
String clipText = ClipBoardManager.getContents();
if (StringUtil.isBlank(clipText)) {
- input.setPromptText(Localization.lang("Please enter the plain references to extract from separated by double empty lines."));
+ input.setPromptText(Localization.lang("Please enter the plain citations to parse from separated by double empty lines."));
} else {
input.setText(clipText);
input.selectAll();
@@ -81,7 +88,7 @@ private void initialize() {
Platform.runLater(() -> {
input.requestFocus();
Button buttonParse = (Button) getDialogPane().lookupButton(parseButtonType);
- buttonParse.setTooltip(new Tooltip((Localization.lang("Starts the extraction and adds the resulting entries to the currently opened database"))));
+ buttonParse.setTooltip(new Tooltip((Localization.lang("Starts the parsing and adds the resulting entries to the currently opened database"))));
buttonParse.setOnAction(event -> viewModel.startParsing());
buttonParse.disableProperty().bind(viewModel.inputTextProperty().isEmpty());
});
diff --git a/src/main/java/org/jabref/gui/plaincitationparser/PlainCitationParserViewModel.java b/src/main/java/org/jabref/gui/plaincitationparser/PlainCitationParserViewModel.java
new file mode 100644
index 00000000000..e4037dd1416
--- /dev/null
+++ b/src/main/java/org/jabref/gui/plaincitationparser/PlainCitationParserViewModel.java
@@ -0,0 +1,122 @@
+package org.jabref.gui.plaincitationparser;
+
+import java.util.List;
+
+import javax.swing.undo.UndoManager;
+
+import javafx.beans.property.ListProperty;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleListProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.beans.property.SimpleStringProperty;
+import javafx.beans.property.StringProperty;
+import javafx.collections.FXCollections;
+
+import org.jabref.gui.DialogService;
+import org.jabref.gui.StateManager;
+import org.jabref.gui.externalfiles.ImportHandler;
+import org.jabref.gui.preferences.GuiPreferences;
+import org.jabref.logic.ai.AiService;
+import org.jabref.logic.importer.FetcherException;
+import org.jabref.logic.importer.plaincitation.GrobidPlainCitationParser;
+import org.jabref.logic.importer.plaincitation.LlmPlainCitationParser;
+import org.jabref.logic.importer.plaincitation.PlainCitationParserChoice;
+import org.jabref.logic.importer.plaincitation.RuleBasedPlainCitationParser;
+import org.jabref.logic.importer.plaincitation.SeveralPlainCitationParser;
+import org.jabref.logic.l10n.Localization;
+import org.jabref.logic.preferences.CliPreferences;
+import org.jabref.logic.util.BackgroundTask;
+import org.jabref.logic.util.TaskExecutor;
+import org.jabref.model.database.BibDatabaseContext;
+import org.jabref.model.util.FileUpdateMonitor;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class PlainCitationParserViewModel {
+ private static final Logger LOGGER = LoggerFactory.getLogger(PlainCitationParserViewModel.class);
+
+ private final DialogService dialogService;
+ private final AiService aiService;
+ private final CliPreferences preferences;
+ private final TaskExecutor taskExecutor;
+ private final ImportHandler importHandler;
+
+ private final StringProperty inputTextProperty = new SimpleStringProperty("");
+ private final ListProperty plainCitationParsers
+ = new SimpleListProperty<>(FXCollections.observableArrayList(List.of(PlainCitationParserChoice.RULE_BASED)));
+ private final ObjectProperty parserChoice;
+
+ public PlainCitationParserViewModel(BibDatabaseContext bibdatabaseContext,
+ DialogService dialogService,
+ AiService aiService,
+ GuiPreferences preferences,
+ FileUpdateMonitor fileUpdateMonitor,
+ TaskExecutor taskExecutor,
+ UndoManager undoManager,
+ StateManager stateManager
+ ) {
+ this.dialogService = dialogService;
+ this.aiService = aiService;
+ this.preferences = preferences;
+ this.taskExecutor = taskExecutor;
+ this.importHandler = new ImportHandler(
+ bibdatabaseContext,
+ preferences,
+ fileUpdateMonitor,
+ undoManager,
+ stateManager,
+ dialogService,
+ taskExecutor);
+
+ if (preferences.getGrobidPreferences().isGrobidEnabled()) {
+ plainCitationParsers.add(PlainCitationParserChoice.GROBID);
+ }
+
+ if (preferences.getAiPreferences().getEnableAi()) {
+ plainCitationParsers.add(PlainCitationParserChoice.LLM);
+ }
+
+ this.parserChoice = new SimpleObjectProperty<>(preferences.getImporterPreferences().defaultPlainCitationParserProperty().get());
+ }
+
+ public void startParsing() {
+ BackgroundTask
+ .wrap(() -> new SeveralPlainCitationParser(
+ switch (parserChoice.get()) {
+ case RULE_BASED ->
+ new RuleBasedPlainCitationParser();
+ case GROBID ->
+ new GrobidPlainCitationParser(preferences.getGrobidPreferences(), preferences.getImportFormatPreferences());
+ case LLM ->
+ new LlmPlainCitationParser(preferences.getImportFormatPreferences(), aiService.getChatLanguageModel());
+ }
+ ).parseSeveralPlainCitations(inputTextProperty.getValue()))
+ .onRunning(() -> dialogService.notify(Localization.lang("Your text is being parsed...")))
+ .onFailure(e -> {
+ if (e instanceof FetcherException) {
+ String msg = Localization.lang("Unable to parse plain citations. Detailed information: %0",
+ e.getMessage());
+ dialogService.notify(msg);
+ } else {
+ LOGGER.warn("Missing exception handling.", e);
+ }
+ })
+ .onSuccess(parsedEntries -> {
+ dialogService.notify(Localization.lang("%0 entries were parsed from your query.", String.valueOf(parsedEntries.size())));
+ importHandler.importEntries(parsedEntries);
+ }).executeWith(taskExecutor);
+ }
+
+ public StringProperty inputTextProperty() {
+ return this.inputTextProperty;
+ }
+
+ public ListProperty plainCitationParsers() {
+ return this.plainCitationParsers;
+ }
+
+ public ObjectProperty parserChoice() {
+ return this.parserChoice;
+ }
+}
diff --git a/src/main/java/org/jabref/gui/preferences/PreferencesDialogView.java b/src/main/java/org/jabref/gui/preferences/PreferencesDialogView.java
index bb246d0e18f..dfebe248e69 100644
--- a/src/main/java/org/jabref/gui/preferences/PreferencesDialogView.java
+++ b/src/main/java/org/jabref/gui/preferences/PreferencesDialogView.java
@@ -88,7 +88,7 @@ private void initialize() {
preferenceTabList.getSelectionModel().clearSelection();
preferenceTabList.getSelectionModel().selectFirst();
});
- searchBox.setPromptText(Localization.lang("Search") + "...");
+ searchBox.setPromptText(Localization.lang("Search..."));
searchBox.setLeft(IconTheme.JabRefIcons.SEARCH.getGraphicNode());
EasyBind.subscribe(preferenceTabList.getSelectionModel().selectedItemProperty(), tab -> {
diff --git a/src/main/java/org/jabref/gui/preferences/PreferencesDialogViewModel.java b/src/main/java/org/jabref/gui/preferences/PreferencesDialogViewModel.java
index d99eeb7fd34..5836422f225 100644
--- a/src/main/java/org/jabref/gui/preferences/PreferencesDialogViewModel.java
+++ b/src/main/java/org/jabref/gui/preferences/PreferencesDialogViewModel.java
@@ -59,12 +59,15 @@ public PreferencesDialogViewModel(DialogService dialogService, GuiPreferences pr
this.dialogService = dialogService;
this.preferences = preferences;
+ // This enables passing unsaved preference values from the AI tab to the "web search" tab.
+ AiTab aiTab = new AiTab();
+
preferenceTabs = FXCollections.observableArrayList(
new GeneralTab(),
new KeyBindingsTab(),
new GroupsTab(),
- new WebSearchTab(),
- new AiTab(),
+ new WebSearchTab(aiTab.aiEnabledProperty()),
+ aiTab,
new EntryTab(),
new TableTab(),
new PreviewTab(),
diff --git a/src/main/java/org/jabref/gui/preferences/ai/AiTab.fxml b/src/main/java/org/jabref/gui/preferences/ai/AiTab.fxml
index c5dfc69e9de..b81bd96ba06 100644
--- a/src/main/java/org/jabref/gui/preferences/ai/AiTab.fxml
+++ b/src/main/java/org/jabref/gui/preferences/ai/AiTab.fxml
@@ -42,6 +42,21 @@
HBox.hgrow="ALWAYS"
maxWidth="Infinity"/>
+
+
+
+
+
+
diff --git a/src/main/java/org/jabref/gui/preferences/ai/AiTab.java b/src/main/java/org/jabref/gui/preferences/ai/AiTab.java
index 4cb67256603..efa406a1d27 100644
--- a/src/main/java/org/jabref/gui/preferences/ai/AiTab.java
+++ b/src/main/java/org/jabref/gui/preferences/ai/AiTab.java
@@ -1,6 +1,7 @@
package org.jabref.gui.preferences.ai;
import javafx.application.Platform;
+import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
@@ -29,6 +30,8 @@ public class AiTab extends AbstractPreferenceTabView implements
private static final String HUGGING_FACE_CHAT_MODEL_PROMPT = "TinyLlama/TinyLlama_v1.1 (or any other model name)";
@FXML private CheckBox enableAi;
+ @FXML private CheckBox autoGenerateEmbeddings;
+ @FXML private CheckBox autoGenerateSummaries;
@FXML private ComboBox aiProviderComboBox;
@FXML private ComboBox chatModelComboBox;
@@ -61,6 +64,10 @@ public void initialize() {
this.viewModel = new AiTabViewModel(preferences);
enableAi.selectedProperty().bindBidirectional(viewModel.enableAi());
+ autoGenerateSummaries.selectedProperty().bindBidirectional(viewModel.autoGenerateSummaries());
+ autoGenerateSummaries.disableProperty().bind(viewModel.disableAutoGenerateSummaries());
+ autoGenerateEmbeddings.selectedProperty().bindBidirectional(viewModel.autoGenerateEmbeddings());
+ autoGenerateEmbeddings.disableProperty().bind(viewModel.disableAutoGenerateEmbeddings());
new ViewModelListCellFactory()
.withText(AiProvider::toString)
@@ -187,4 +194,8 @@ public String getTabName() {
private void onResetExpertSettingsButtonClick() {
viewModel.resetExpertSettings();
}
+
+ public ReadOnlyBooleanProperty aiEnabledProperty() {
+ return enableAi.selectedProperty();
+ }
}
diff --git a/src/main/java/org/jabref/gui/preferences/ai/AiTabViewModel.java b/src/main/java/org/jabref/gui/preferences/ai/AiTabViewModel.java
index 8ba7c3a66c0..c1c4ef5f6a5 100644
--- a/src/main/java/org/jabref/gui/preferences/ai/AiTabViewModel.java
+++ b/src/main/java/org/jabref/gui/preferences/ai/AiTabViewModel.java
@@ -2,7 +2,6 @@
import java.util.List;
import java.util.Locale;
-import java.util.Map;
import java.util.Objects;
import javafx.beans.property.BooleanProperty;
@@ -37,6 +36,10 @@ public class AiTabViewModel implements PreferenceTabViewModel {
private final Locale oldLocale;
private final BooleanProperty enableAi = new SimpleBooleanProperty();
+ private final BooleanProperty autoGenerateEmbeddings = new SimpleBooleanProperty();
+ private final BooleanProperty disableAutoGenerateEmbeddings = new SimpleBooleanProperty();
+ private final BooleanProperty autoGenerateSummaries = new SimpleBooleanProperty();
+ private final BooleanProperty disableAutoGenerateSummaries = new SimpleBooleanProperty();
private final ListProperty aiProvidersList =
new SimpleListProperty<>(FXCollections.observableArrayList(AiProvider.values()));
@@ -115,7 +118,7 @@ public AiTabViewModel(CliPreferences preferences) {
);
this.selectedAiProvider.addListener((observable, oldValue, newValue) -> {
- List models = AiDefaultPreferences.AVAILABLE_CHAT_MODELS.get(newValue);
+ List models = AiDefaultPreferences.getAvailableModels(newValue);
// When we setAll on Hugging Face, models are empty, and currentChatModel become null.
// It becomes null beause currentChatModel is binded to combobox, and this combobox becomes empty.
@@ -182,14 +185,7 @@ public AiTabViewModel(CliPreferences preferences) {
case HUGGING_FACE -> huggingFaceChatModel.set(newValue);
}
- Map modelContextWindows = AiDefaultPreferences.CONTEXT_WINDOW_SIZES.get(selectedAiProvider.get());
-
- if (modelContextWindows == null) {
- contextWindowSize.set(AiDefaultPreferences.CONTEXT_WINDOW_SIZE);
- return;
- }
-
- contextWindowSize.set(modelContextWindows.getOrDefault(newValue, AiDefaultPreferences.CONTEXT_WINDOW_SIZE));
+ contextWindowSize.set(AiDefaultPreferences.getContextWindowSize(selectedAiProvider.get(), newValue));
});
this.currentApiKey.addListener((observable, oldValue, newValue) -> {
@@ -295,6 +291,8 @@ public void setValues() {
huggingFaceChatModel.setValue(aiPreferences.getHuggingFaceChatModel());
enableAi.setValue(aiPreferences.getEnableAi());
+ autoGenerateSummaries.setValue(aiPreferences.getAutoGenerateSummaries());
+ autoGenerateEmbeddings.setValue(aiPreferences.getAutoGenerateEmbeddings());
selectedAiProvider.setValue(aiPreferences.getAiProvider());
@@ -313,6 +311,8 @@ public void setValues() {
@Override
public void storeSettings() {
aiPreferences.setEnableAi(enableAi.get());
+ aiPreferences.setAutoGenerateEmbeddings(autoGenerateEmbeddings.get());
+ aiPreferences.setAutoGenerateSummaries(autoGenerateSummaries.get());
aiPreferences.setAiProvider(selectedAiProvider.get());
@@ -348,13 +348,12 @@ public void storeSettings() {
}
public void resetExpertSettings() {
- String resetApiBaseUrl = AiDefaultPreferences.PROVIDERS_API_URLS.get(selectedAiProvider.get());
+ String resetApiBaseUrl = selectedAiProvider.get().getApiUrl();
currentApiBaseUrl.set(resetApiBaseUrl);
instruction.set(AiDefaultPreferences.SYSTEM_MESSAGE);
- int resetContextWindowSize = AiDefaultPreferences.CONTEXT_WINDOW_SIZES.getOrDefault(selectedAiProvider.get(), Map.of()).getOrDefault(currentChatModel.get(), 0);
- contextWindowSize.set(resetContextWindowSize);
+ contextWindowSize.set(AiDefaultPreferences.getContextWindowSize(selectedAiProvider.get(), currentChatModel.get()));
temperature.set(LocalizedNumbers.doubleToString(AiDefaultPreferences.TEMPERATURE));
documentSplitterChunkSize.set(AiDefaultPreferences.DOCUMENT_SPLITTER_CHUNK_SIZE);
@@ -407,6 +406,22 @@ public BooleanProperty enableAi() {
return enableAi;
}
+ public BooleanProperty autoGenerateEmbeddings() {
+ return autoGenerateEmbeddings;
+ }
+
+ public BooleanProperty disableAutoGenerateEmbeddings() {
+ return disableAutoGenerateEmbeddings;
+ }
+
+ public BooleanProperty autoGenerateSummaries() {
+ return autoGenerateSummaries;
+ }
+
+ public BooleanProperty disableAutoGenerateSummaries() {
+ return disableAutoGenerateSummaries;
+ }
+
public ReadOnlyListProperty aiProvidersProperty() {
return aiProvidersList;
}
diff --git a/src/main/java/org/jabref/gui/preferences/journals/JournalAbbreviationsTab.java b/src/main/java/org/jabref/gui/preferences/journals/JournalAbbreviationsTab.java
index 1ca3985fc68..5bfa504edf5 100644
--- a/src/main/java/org/jabref/gui/preferences/journals/JournalAbbreviationsTab.java
+++ b/src/main/java/org/jabref/gui/preferences/journals/JournalAbbreviationsTab.java
@@ -85,7 +85,7 @@ private void initialize() {
setBindings();
setAnimations();
- searchBox.setPromptText(Localization.lang("Search") + "...");
+ searchBox.setPromptText(Localization.lang("Search..."));
searchBox.setLeft(IconTheme.JabRefIcons.SEARCH.getGraphicNode());
}
@@ -139,7 +139,7 @@ private void setBindings() {
private void setAnimations() {
ObjectProperty flashingColor = new SimpleObjectProperty<>(Color.TRANSPARENT);
- StringProperty flashingColorStringProperty = createFlashingColorStringProperty(flashingColor);
+ StringProperty flashingColorStringProperty = ColorUtil.createFlashingColorStringProperty(flashingColor);
searchBox.styleProperty().bind(
new SimpleStringProperty("-fx-control-inner-background: ").concat(flashingColorStringProperty).concat(";")
@@ -183,17 +183,6 @@ private void addAbbreviationActions() {
editAbbreviation();
}
- private static StringProperty createFlashingColorStringProperty(final ObjectProperty flashingColor) {
- final StringProperty flashingColorStringProperty = new SimpleStringProperty();
- setColorStringFromColor(flashingColorStringProperty, flashingColor);
- flashingColor.addListener((observable, oldValue, newValue) -> setColorStringFromColor(flashingColorStringProperty, flashingColor));
- return flashingColorStringProperty;
- }
-
- private static void setColorStringFromColor(StringProperty colorStringProperty, ObjectProperty color) {
- colorStringProperty.set(ColorUtil.toRGBACode(color.get()));
- }
-
@FXML
private void editAbbreviation() {
journalAbbreviationsTable.edit(
diff --git a/src/main/java/org/jabref/gui/preferences/keybindings/KeyBindingsTab.fxml b/src/main/java/org/jabref/gui/preferences/keybindings/KeyBindingsTab.fxml
index e20461d98e4..7713a8fd7b9 100644
--- a/src/main/java/org/jabref/gui/preferences/keybindings/KeyBindingsTab.fxml
+++ b/src/main/java/org/jabref/gui/preferences/keybindings/KeyBindingsTab.fxml
@@ -1,5 +1,6 @@
+
@@ -8,23 +9,32 @@
+
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/java/org/jabref/gui/preferences/keybindings/KeyBindingsTab.java b/src/main/java/org/jabref/gui/preferences/keybindings/KeyBindingsTab.java
index 20bd5728325..e58baa45f2e 100644
--- a/src/main/java/org/jabref/gui/preferences/keybindings/KeyBindingsTab.java
+++ b/src/main/java/org/jabref/gui/preferences/keybindings/KeyBindingsTab.java
@@ -1,5 +1,9 @@
package org.jabref.gui.preferences.keybindings;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.beans.property.SimpleStringProperty;
+import javafx.beans.property.StringProperty;
import javafx.fxml.FXML;
import javafx.scene.control.MenuButton;
import javafx.scene.control.MenuItem;
@@ -8,12 +12,15 @@
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeTableColumn;
import javafx.scene.control.TreeTableView;
+import javafx.scene.paint.Color;
+import org.jabref.gui.icon.IconTheme;
import org.jabref.gui.icon.JabRefIcon;
import org.jabref.gui.keyboard.KeyBindingRepository;
import org.jabref.gui.preferences.AbstractPreferenceTabView;
import org.jabref.gui.preferences.PreferencesTab;
import org.jabref.gui.preferences.keybindings.presets.KeyBindingPreset;
+import org.jabref.gui.util.ColorUtil;
import org.jabref.gui.util.RecursiveTreeItem;
import org.jabref.gui.util.ViewModelTreeTableCellFactory;
import org.jabref.logic.l10n.Localization;
@@ -21,9 +28,11 @@
import com.airhacks.afterburner.views.ViewLoader;
import com.tobiasdiez.easybind.EasyBind;
import jakarta.inject.Inject;
+import org.controlsfx.control.textfield.CustomTextField;
public class KeyBindingsTab extends AbstractPreferenceTabView implements PreferencesTab {
+ @FXML private CustomTextField searchBox;
@FXML private TreeTableView keyBindingsTable;
@FXML private TreeTableColumn actionColumn;
@FXML private TreeTableColumn shortcutColumn;
@@ -71,6 +80,19 @@ private void initialize() {
.install(clearColumn);
viewModel.keyBindingPresets().forEach(preset -> presetsButton.getItems().add(createMenuItem(preset)));
+
+ searchBox.textProperty().addListener((observable, previousText, searchTerm) ->
+ viewModel.filterValues(searchTerm));
+
+ ObjectProperty flashingColor = new SimpleObjectProperty<>(Color.TRANSPARENT);
+ StringProperty flashingColorStringProperty = ColorUtil.createFlashingColorStringProperty(flashingColor);
+
+ searchBox.styleProperty().bind(
+ new SimpleStringProperty("-fx-control-inner-background: ").concat(flashingColorStringProperty).concat(";")
+ );
+
+ searchBox.setPromptText(Localization.lang("Search..."));
+ searchBox.setLeft(IconTheme.JabRefIcons.SEARCH.getGraphicNode());
}
private MenuItem createMenuItem(KeyBindingPreset preset) {
diff --git a/src/main/java/org/jabref/gui/preferences/keybindings/KeyBindingsTabViewModel.java b/src/main/java/org/jabref/gui/preferences/keybindings/KeyBindingsTabViewModel.java
index 0af50012a23..0f114d40346 100644
--- a/src/main/java/org/jabref/gui/preferences/keybindings/KeyBindingsTabViewModel.java
+++ b/src/main/java/org/jabref/gui/preferences/keybindings/KeyBindingsTabViewModel.java
@@ -53,17 +53,28 @@ public KeyBindingsTabViewModel(KeyBindingRepository keyBindingRepository, Dialog
@Override
public void setValues() {
KeyBindingViewModel root = new KeyBindingViewModel(keyBindingRepository, KeyBindingCategory.FILE);
+ rootKeyBinding.set(root);
+ filterValues("");
+ }
+
+ public void filterValues(String term) {
+ KeyBindingViewModel root = rootKeyBinding.get();
+ root.clear();
+ root.getChildren().clear();
for (KeyBindingCategory category : KeyBindingCategory.values()) {
KeyBindingViewModel categoryItem = new KeyBindingViewModel(keyBindingRepository, category);
keyBindingRepository.getKeyBindings().forEach((keyBinding, bind) -> {
- if (keyBinding.getCategory() == category) {
+ if (keyBinding.getCategory() == category &&
+ (keyBinding.getLocalization().toLowerCase().contains(term.toLowerCase()) ||
+ keyBinding.getCategory().getName().toLowerCase().contains(term.toLowerCase()))) {
KeyBindingViewModel keyBindViewModel = new KeyBindingViewModel(keyBindingRepository, keyBinding, bind);
categoryItem.getChildren().add(keyBindViewModel);
}
});
- root.getChildren().add(categoryItem);
+ if (!categoryItem.getChildren().isEmpty()) {
+ root.getChildren().add(categoryItem);
+ }
}
- rootKeyBinding.set(root);
}
public void setNewBindingForCurrent(KeyEvent event) {
diff --git a/src/main/java/org/jabref/gui/preferences/preview/PreviewTab.java b/src/main/java/org/jabref/gui/preferences/preview/PreviewTab.java
index baa36aa4b11..134f290b763 100644
--- a/src/main/java/org/jabref/gui/preferences/preview/PreviewTab.java
+++ b/src/main/java/org/jabref/gui/preferences/preview/PreviewTab.java
@@ -130,7 +130,7 @@ public void initialize() {
showAsTabCheckBox.selectedProperty().bindBidirectional(viewModel.showAsExtraTabProperty());
showPreviewTooltipCheckBox.selectedProperty().bindBidirectional(viewModel.showPreviewInEntryTableTooltip());
- searchBox.setPromptText(Localization.lang("Search") + "...");
+ searchBox.setPromptText(Localization.lang("Search..."));
searchBox.setLeft(IconTheme.JabRefIcons.SEARCH.getGraphicNode());
ActionFactory factory = new ActionFactory();
diff --git a/src/main/java/org/jabref/gui/preferences/websearch/WebSearchTab.fxml b/src/main/java/org/jabref/gui/preferences/websearch/WebSearchTab.fxml
index fc4fd940e07..870b9633ac5 100644
--- a/src/main/java/org/jabref/gui/preferences/websearch/WebSearchTab.fxml
+++ b/src/main/java/org/jabref/gui/preferences/websearch/WebSearchTab.fxml
@@ -10,6 +10,7 @@
+
@@ -21,6 +22,10 @@
+
+
+
+
diff --git a/src/main/java/org/jabref/gui/preferences/websearch/WebSearchTab.java b/src/main/java/org/jabref/gui/preferences/websearch/WebSearchTab.java
index b15196a59e3..b981b4148d1 100644
--- a/src/main/java/org/jabref/gui/preferences/websearch/WebSearchTab.java
+++ b/src/main/java/org/jabref/gui/preferences/websearch/WebSearchTab.java
@@ -1,9 +1,11 @@
package org.jabref.gui.preferences.websearch;
import javafx.beans.InvalidationListener;
+import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
+import javafx.scene.control.ComboBox;
import javafx.scene.control.SplitPane;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
@@ -16,7 +18,9 @@
import org.jabref.gui.preferences.AbstractPreferenceTabView;
import org.jabref.gui.preferences.PreferencesTab;
import org.jabref.gui.slr.StudyCatalogItem;
+import org.jabref.gui.util.ViewModelListCellFactory;
import org.jabref.gui.util.ViewModelTableRowFactory;
+import org.jabref.logic.importer.plaincitation.PlainCitationParserChoice;
import org.jabref.logic.l10n.Localization;
import org.jabref.logic.preferences.FetcherApiKey;
@@ -30,6 +34,7 @@ public class WebSearchTab extends AbstractPreferenceTabView defaultPlainCitationParser;
@FXML private CheckBox useCustomDOI;
@FXML private TextField useCustomDOIName;
@@ -49,7 +54,11 @@ public class WebSearchTab extends AbstractPreferenceTabView catalogEnabledColumn;
@FXML private TableColumn catalogColumn;
- public WebSearchTab() {
+ private final ReadOnlyBooleanProperty refAiEnabled;
+
+ public WebSearchTab(ReadOnlyBooleanProperty refAiEnabled) {
+ this.refAiEnabled = refAiEnabled;
+
ViewLoader.view(this)
.root(this)
.load();
@@ -61,7 +70,7 @@ public String getTabName() {
}
public void initialize() {
- this.viewModel = new WebSearchTabViewModel(preferences, dialogService);
+ this.viewModel = new WebSearchTabViewModel(preferences, dialogService, refAiEnabled);
enableWebSearch.selectedProperty().bindBidirectional(viewModel.enableWebSearchProperty());
generateNewKeyOnImport.selectedProperty().bindBidirectional(viewModel.generateKeyOnImportProperty());
@@ -69,6 +78,12 @@ public void initialize() {
downloadLinkedOnlineFiles.selectedProperty().bindBidirectional(viewModel.shouldDownloadLinkedOnlineFiles());
keepDownloadUrl.selectedProperty().bindBidirectional(viewModel.shouldKeepDownloadUrl());
+ new ViewModelListCellFactory()
+ .withText(PlainCitationParserChoice::getLocalizedName)
+ .install(defaultPlainCitationParser);
+ defaultPlainCitationParser.itemsProperty().bind(viewModel.plainCitationParsers());
+ defaultPlainCitationParser.valueProperty().bindBidirectional(viewModel.defaultPlainCitationParserProperty());
+
grobidEnabled.selectedProperty().bindBidirectional(viewModel.grobidEnabledProperty());
grobidURL.textProperty().bindBidirectional(viewModel.grobidURLProperty());
grobidURL.disableProperty().bind(grobidEnabled.selectedProperty().not());
diff --git a/src/main/java/org/jabref/gui/preferences/websearch/WebSearchTabViewModel.java b/src/main/java/org/jabref/gui/preferences/websearch/WebSearchTabViewModel.java
index 81cc3a2a11a..37f33badb6a 100644
--- a/src/main/java/org/jabref/gui/preferences/websearch/WebSearchTabViewModel.java
+++ b/src/main/java/org/jabref/gui/preferences/websearch/WebSearchTabViewModel.java
@@ -27,7 +27,8 @@
import org.jabref.logic.importer.WebFetchers;
import org.jabref.logic.importer.fetcher.CompositeSearchBasedFetcher;
import org.jabref.logic.importer.fetcher.CustomizableKeyFetcher;
-import org.jabref.logic.importer.fetcher.GrobidPreferences;
+import org.jabref.logic.importer.plaincitation.PlainCitationParserChoice;
+import org.jabref.logic.importer.util.GrobidPreferences;
import org.jabref.logic.l10n.Localization;
import org.jabref.logic.net.URLDownload;
import org.jabref.logic.os.OS;
@@ -44,6 +45,10 @@ public class WebSearchTabViewModel implements PreferenceTabViewModel {
private final BooleanProperty shouldDownloadLinkedOnlineFiles = new SimpleBooleanProperty();
private final BooleanProperty shouldkeepDownloadUrl = new SimpleBooleanProperty();
+ private final ListProperty plainCitationParsers =
+ new SimpleListProperty<>(FXCollections.observableArrayList(PlainCitationParserChoice.values()));
+ private final ObjectProperty defaultPlainCitationParser = new SimpleObjectProperty<>();
+
private final BooleanProperty useCustomDOIProperty = new SimpleBooleanProperty();
private final StringProperty useCustomDOINameProperty = new SimpleStringProperty("");
@@ -64,7 +69,9 @@ public class WebSearchTabViewModel implements PreferenceTabViewModel {
private final FilePreferences filePreferences;
private final ImportFormatPreferences importFormatPreferences;
- public WebSearchTabViewModel(CliPreferences preferences, DialogService dialogService) {
+ private final ReadOnlyBooleanProperty refAiEnabled;
+
+ public WebSearchTabViewModel(CliPreferences preferences, DialogService dialogService, ReadOnlyBooleanProperty refAiEnabled) {
this.dialogService = dialogService;
this.preferences = preferences;
this.importerPreferences = preferences.getImporterPreferences();
@@ -72,6 +79,48 @@ public WebSearchTabViewModel(CliPreferences preferences, DialogService dialogSer
this.doiPreferences = preferences.getDOIPreferences();
this.filePreferences = preferences.getFilePreferences();
this.importFormatPreferences = preferences.getImportFormatPreferences();
+
+ this.refAiEnabled = refAiEnabled;
+
+ setupPlainCitationParsers(preferences);
+ }
+
+ private void setupPlainCitationParsers(CliPreferences preferences) {
+ if (!refAiEnabled.get()) {
+ plainCitationParsers.remove(PlainCitationParserChoice.LLM);
+ }
+
+ refAiEnabled.addListener((observable, oldValue, newValue) -> {
+ if (newValue) {
+ plainCitationParsers.add(PlainCitationParserChoice.LLM);
+ } else {
+ PlainCitationParserChoice oldChoice = defaultPlainCitationParser.get();
+
+ plainCitationParsers.remove(PlainCitationParserChoice.LLM);
+
+ if (oldChoice == PlainCitationParserChoice.LLM) {
+ defaultPlainCitationParser.set(plainCitationParsers.getFirst());
+ }
+ }
+ });
+
+ if (!grobidEnabledProperty().get()) {
+ plainCitationParsers.remove(PlainCitationParserChoice.GROBID);
+ }
+
+ grobidEnabledProperty.addListener((observable, oldValue, newValue) -> {
+ if (newValue) {
+ plainCitationParsers.add(PlainCitationParserChoice.GROBID);
+ } else {
+ PlainCitationParserChoice oldChoice = defaultPlainCitationParser.get();
+
+ plainCitationParsers.remove(PlainCitationParserChoice.GROBID);
+
+ if (oldChoice == PlainCitationParserChoice.GROBID) {
+ defaultPlainCitationParser.set(plainCitationParsers.getFirst());
+ }
+ }
+ });
}
@Override
@@ -81,6 +130,8 @@ public void setValues() {
warnAboutDuplicatesOnImportProperty.setValue(importerPreferences.shouldWarnAboutDuplicatesOnImport());
shouldDownloadLinkedOnlineFiles.setValue(filePreferences.shouldDownloadLinkedFiles());
shouldkeepDownloadUrl.setValue(filePreferences.shouldKeepDownloadUrl());
+ defaultPlainCitationParser.setValue(importerPreferences.getDefaultPlainCitationParser());
+
useCustomDOIProperty.setValue(doiPreferences.isUseCustom());
useCustomDOINameProperty.setValue(doiPreferences.getDefaultBaseURI());
@@ -108,6 +159,7 @@ public void storeSettings() {
importerPreferences.setWarnAboutDuplicatesOnImport(warnAboutDuplicatesOnImportProperty.getValue());
filePreferences.setDownloadLinkedFiles(shouldDownloadLinkedOnlineFiles.getValue());
filePreferences.setKeepDownloadUrl(shouldkeepDownloadUrl.getValue());
+ importerPreferences.setDefaultPlainCitationParser(defaultPlainCitationParser.getValue());
grobidPreferences.setGrobidEnabled(grobidEnabledProperty.getValue());
grobidPreferences.setGrobidOptOut(grobidPreferences.isGrobidOptOut());
grobidPreferences.setGrobidURL(grobidURLProperty.getValue());
@@ -133,6 +185,14 @@ public BooleanProperty generateKeyOnImportProperty() {
return generateKeyOnImportProperty;
}
+ public ListProperty plainCitationParsers() {
+ return plainCitationParsers;
+ }
+
+ public ObjectProperty defaultPlainCitationParserProperty() {
+ return defaultPlainCitationParser;
+ }
+
public BooleanProperty useCustomDOIProperty() {
return this.useCustomDOIProperty;
}
diff --git a/src/main/java/org/jabref/gui/remote/CLIMessageHandler.java b/src/main/java/org/jabref/gui/remote/CLIMessageHandler.java
index e42bcc2e358..4da679c147c 100644
--- a/src/main/java/org/jabref/gui/remote/CLIMessageHandler.java
+++ b/src/main/java/org/jabref/gui/remote/CLIMessageHandler.java
@@ -41,7 +41,6 @@ public void handleCommandLineArguments(String[] message) {
message,
ArgumentProcessor.Mode.REMOTE_START,
preferences,
- preferences,
fileUpdateMonitor,
entryTypesManager);
argumentProcessor.processArguments();
diff --git a/src/main/java/org/jabref/gui/search/GlobalSearchBar.java b/src/main/java/org/jabref/gui/search/GlobalSearchBar.java
index cc87b5cfd17..a4cad4a6ccb 100644
--- a/src/main/java/org/jabref/gui/search/GlobalSearchBar.java
+++ b/src/main/java/org/jabref/gui/search/GlobalSearchBar.java
@@ -285,7 +285,7 @@ private void initSearchModifierButtons() {
searchField.requestFocus();
});
- openGlobalSearchButton.disableProperty().bindBidirectional(globalSearchActive);
+ openGlobalSearchButton.disableProperty().bind(globalSearchActive.or(needsDatabase(stateManager).not()));
openGlobalSearchButton.setTooltip(new Tooltip(Localization.lang("Search across libraries in a new window")));
initSearchModifierButton(openGlobalSearchButton);
openGlobalSearchButton.setOnAction(evt -> openGlobalSearchDialog());
diff --git a/src/main/java/org/jabref/gui/shared/SharedDatabaseUIManager.java b/src/main/java/org/jabref/gui/shared/SharedDatabaseUIManager.java
index d3f2989b36e..c1ecff23cfb 100644
--- a/src/main/java/org/jabref/gui/shared/SharedDatabaseUIManager.java
+++ b/src/main/java/org/jabref/gui/shared/SharedDatabaseUIManager.java
@@ -175,6 +175,7 @@ public LibraryTab openNewSharedDatabaseTab(DBMSConnectionProperties dbmsConnecti
bibDatabaseContext,
tabContainer,
dialogService,
+ aiService,
preferences,
stateManager,
fileUpdateMonitor,
diff --git a/src/main/java/org/jabref/gui/sidepane/SidePane.java b/src/main/java/org/jabref/gui/sidepane/SidePane.java
index e4a969e663b..457a9f28c65 100644
--- a/src/main/java/org/jabref/gui/sidepane/SidePane.java
+++ b/src/main/java/org/jabref/gui/sidepane/SidePane.java
@@ -15,8 +15,8 @@
import org.jabref.gui.LibraryTabContainer;
import org.jabref.gui.StateManager;
import org.jabref.gui.actions.SimpleCommand;
-import org.jabref.gui.ai.chatting.chathistory.ChatHistoryService;
import org.jabref.gui.preferences.GuiPreferences;
+import org.jabref.logic.ai.AiService;
import org.jabref.logic.journals.JournalAbbreviationRepository;
import org.jabref.logic.util.TaskExecutor;
import org.jabref.model.entry.BibEntryTypesManager;
@@ -33,10 +33,10 @@ public class SidePane extends VBox {
public SidePane(LibraryTabContainer tabContainer,
GuiPreferences preferences,
- ChatHistoryService chatHistoryService,
JournalAbbreviationRepository abbreviationRepository,
TaskExecutor taskExecutor,
DialogService dialogService,
+ AiService aiService,
StateManager stateManager,
FileUpdateMonitor fileUpdateMonitor,
BibEntryTypesManager entryTypesManager,
@@ -47,11 +47,11 @@ public SidePane(LibraryTabContainer tabContainer,
this.viewModel = new SidePaneViewModel(
tabContainer,
preferences,
- chatHistoryService,
abbreviationRepository,
stateManager,
taskExecutor,
dialogService,
+ aiService,
fileUpdateMonitor,
entryTypesManager,
clipBoardManager,
diff --git a/src/main/java/org/jabref/gui/sidepane/SidePaneContentFactory.java b/src/main/java/org/jabref/gui/sidepane/SidePaneContentFactory.java
index 171fc8b8777..d8eee75d756 100644
--- a/src/main/java/org/jabref/gui/sidepane/SidePaneContentFactory.java
+++ b/src/main/java/org/jabref/gui/sidepane/SidePaneContentFactory.java
@@ -8,12 +8,12 @@
import org.jabref.gui.DialogService;
import org.jabref.gui.LibraryTabContainer;
import org.jabref.gui.StateManager;
-import org.jabref.gui.ai.chatting.chathistory.ChatHistoryService;
import org.jabref.gui.groups.GroupTreeView;
import org.jabref.gui.importer.fetcher.WebSearchPaneView;
import org.jabref.gui.openoffice.OpenOfficePanel;
import org.jabref.gui.preferences.GuiPreferences;
import org.jabref.gui.util.UiTaskExecutor;
+import org.jabref.logic.ai.AiService;
import org.jabref.logic.journals.JournalAbbreviationRepository;
import org.jabref.logic.util.TaskExecutor;
import org.jabref.model.entry.BibEntryTypesManager;
@@ -22,10 +22,10 @@
public class SidePaneContentFactory {
private final LibraryTabContainer tabContainer;
private final GuiPreferences preferences;
- private final ChatHistoryService chatHistoryService;
private final JournalAbbreviationRepository abbreviationRepository;
private final TaskExecutor taskExecutor;
private final DialogService dialogService;
+ private final AiService aiService;
private final StateManager stateManager;
private final FileUpdateMonitor fileUpdateMonitor;
private final BibEntryTypesManager entryTypesManager;
@@ -34,10 +34,10 @@ public class SidePaneContentFactory {
public SidePaneContentFactory(LibraryTabContainer tabContainer,
GuiPreferences preferences,
- ChatHistoryService chatHistoryService,
JournalAbbreviationRepository abbreviationRepository,
TaskExecutor taskExecutor,
DialogService dialogService,
+ AiService aiService,
StateManager stateManager,
FileUpdateMonitor fileUpdateMonitor,
BibEntryTypesManager entryTypesManager,
@@ -45,10 +45,10 @@ public SidePaneContentFactory(LibraryTabContainer tabContainer,
UndoManager undoManager) {
this.tabContainer = tabContainer;
this.preferences = preferences;
- this.chatHistoryService = chatHistoryService;
this.abbreviationRepository = abbreviationRepository;
this.taskExecutor = taskExecutor;
this.dialogService = dialogService;
+ this.aiService = aiService;
this.stateManager = stateManager;
this.fileUpdateMonitor = fileUpdateMonitor;
this.entryTypesManager = entryTypesManager;
@@ -63,7 +63,7 @@ public Node create(SidePaneType sidePaneType) {
stateManager,
preferences,
dialogService,
- chatHistoryService);
+ aiService);
case OPEN_OFFICE -> new OpenOfficePanel(
tabContainer,
preferences,
@@ -74,6 +74,7 @@ public Node create(SidePaneType sidePaneType) {
abbreviationRepository,
(UiTaskExecutor) taskExecutor,
dialogService,
+ aiService,
stateManager,
fileUpdateMonitor,
entryTypesManager,
diff --git a/src/main/java/org/jabref/gui/sidepane/SidePaneViewModel.java b/src/main/java/org/jabref/gui/sidepane/SidePaneViewModel.java
index 9d13d9ef56a..b4d13c70d9f 100644
--- a/src/main/java/org/jabref/gui/sidepane/SidePaneViewModel.java
+++ b/src/main/java/org/jabref/gui/sidepane/SidePaneViewModel.java
@@ -19,9 +19,9 @@
import org.jabref.gui.LibraryTabContainer;
import org.jabref.gui.StateManager;
import org.jabref.gui.actions.SimpleCommand;
-import org.jabref.gui.ai.chatting.chathistory.ChatHistoryService;
import org.jabref.gui.frame.SidePanePreferences;
import org.jabref.gui.preferences.GuiPreferences;
+import org.jabref.logic.ai.AiService;
import org.jabref.logic.journals.JournalAbbreviationRepository;
import org.jabref.logic.util.TaskExecutor;
import org.jabref.model.entry.BibEntryTypesManager;
@@ -42,11 +42,11 @@ public class SidePaneViewModel extends AbstractViewModel {
public SidePaneViewModel(LibraryTabContainer tabContainer,
GuiPreferences preferences,
- ChatHistoryService chatHistoryService,
JournalAbbreviationRepository abbreviationRepository,
StateManager stateManager,
TaskExecutor taskExecutor,
DialogService dialogService,
+ AiService aiService,
FileUpdateMonitor fileUpdateMonitor,
BibEntryTypesManager entryTypesManager,
ClipBoardManager clipBoardManager,
@@ -57,10 +57,10 @@ public SidePaneViewModel(LibraryTabContainer tabContainer,
this.sidePaneContentFactory = new SidePaneContentFactory(
tabContainer,
preferences,
- chatHistoryService,
abbreviationRepository,
taskExecutor,
dialogService,
+ aiService,
stateManager,
fileUpdateMonitor,
entryTypesManager,
diff --git a/src/main/java/org/jabref/gui/undo/CountingUndoManager.java b/src/main/java/org/jabref/gui/undo/CountingUndoManager.java
index 1e4eaa8981d..6a0c4a557e5 100644
--- a/src/main/java/org/jabref/gui/undo/CountingUndoManager.java
+++ b/src/main/java/org/jabref/gui/undo/CountingUndoManager.java
@@ -10,13 +10,15 @@
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleIntegerProperty;
+import org.jabref.gui.util.UiTaskExecutor;
+
public class CountingUndoManager extends UndoManager {
private int unchangedPoint;
/**
* Indicates the number of edits aka balance of edits on the stack +1 when an edit is added/redone and -1 when an edit is undoed.
- * */
+ */
private final IntegerProperty balanceProperty = new SimpleIntegerProperty(0);
private final BooleanProperty undoableProperty = new SimpleBooleanProperty(false);
private final BooleanProperty redoableProperty = new SimpleBooleanProperty(false);
@@ -67,11 +69,11 @@ private void decrementBalance() {
}
private void updateUndoableStatus() {
- undoableProperty.setValue(canUndo());
+ UiTaskExecutor.runInJavaFXThread(() -> undoableProperty.setValue(canUndo()));
}
private void updateRedoableStatus() {
- redoableProperty.setValue(canRedo());
+ UiTaskExecutor.runInJavaFXThread(() -> redoableProperty.setValue(canRedo()));
}
public ReadOnlyBooleanProperty getUndoableProperty() {
diff --git a/src/main/java/org/jabref/gui/util/ColorUtil.java b/src/main/java/org/jabref/gui/util/ColorUtil.java
index 31c64942c47..27d89e6db5b 100644
--- a/src/main/java/org/jabref/gui/util/ColorUtil.java
+++ b/src/main/java/org/jabref/gui/util/ColorUtil.java
@@ -1,5 +1,8 @@
package org.jabref.gui.util;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleStringProperty;
+import javafx.beans.property.StringProperty;
import javafx.scene.paint.Color;
public class ColorUtil {
@@ -22,4 +25,15 @@ public static String toRGBACode(Color color) {
public static String toHex(Color validFieldBackgroundColor) {
return "#%02x%02x%02x".formatted((int) validFieldBackgroundColor.getRed(), (int) validFieldBackgroundColor.getGreen(), (int) validFieldBackgroundColor.getBlue());
}
+
+ public static StringProperty createFlashingColorStringProperty(final ObjectProperty flashingColor) {
+ final StringProperty flashingColorStringProperty = new SimpleStringProperty();
+ setColorStringFromColor(flashingColorStringProperty, flashingColor);
+ flashingColor.addListener((observable, oldValue, newValue) -> setColorStringFromColor(flashingColorStringProperty, flashingColor));
+ return flashingColorStringProperty;
+ }
+
+ public static void setColorStringFromColor(StringProperty colorStringProperty, ObjectProperty color) {
+ colorStringProperty.set(ColorUtil.toRGBACode(color.get()));
+ }
}
diff --git a/src/main/java/org/jabref/logic/ai/AiDefaultPreferences.java b/src/main/java/org/jabref/logic/ai/AiDefaultPreferences.java
index b17fa670867..e83c1a084b5 100644
--- a/src/main/java/org/jabref/logic/ai/AiDefaultPreferences.java
+++ b/src/main/java/org/jabref/logic/ai/AiDefaultPreferences.java
@@ -1,5 +1,6 @@
package org.jabref.logic.ai;
+import java.util.Arrays;
import java.util.List;
import java.util.Map;
@@ -7,59 +8,61 @@
import org.jabref.model.ai.EmbeddingModel;
public class AiDefaultPreferences {
- public static final Map> AVAILABLE_CHAT_MODELS = Map.of(
- AiProvider.OPEN_AI, List.of("gpt-4o-mini", "gpt-4o", "gpt-4", "gpt-4-turbo", "gpt-3.5-turbo"),
- // "mistral" and "mixtral" are not language mistakes.
- AiProvider.MISTRAL_AI, List.of("open-mistral-nemo", "open-mistral-7b", "open-mixtral-8x7b", "open-mixtral-8x22b", "mistral-large-latest"),
- AiProvider.GEMINI, List.of("gemini-1.5-flash", "gemini-1.5-pro", "gemini-1.0-pro"),
- AiProvider.HUGGING_FACE, List.of()
- );
+ public enum PredefinedChatModel {
+ GPT_4O_MINI(AiProvider.OPEN_AI, "gpt-4o-mini", 128000),
+ GPT_4O(AiProvider.OPEN_AI, "gpt-4o", 128000),
+ GPT_4(AiProvider.OPEN_AI, "gpt-4", 8192),
+ GPT_4_TURBO(AiProvider.OPEN_AI, "gpt-4-turbo", 128000),
+ GPT_3_5_TURBO(AiProvider.OPEN_AI, "gpt-3.5-turbo", 16385),
+ OPEN_MISTRAL_NEMO(AiProvider.MISTRAL_AI, "open-mistral-nemo", 128000),
+ OPEN_MISTRAL_7B(AiProvider.MISTRAL_AI, "open-mistral-7b", 32000),
+ // "mixtral" is not a typo.
+ OPEN_MIXTRAL_8X7B(AiProvider.MISTRAL_AI, "open-mixtral-8x7b", 32000),
+ OPEN_MIXTRAL_8X22B(AiProvider.MISTRAL_AI, "open-mixtral-8x22b", 64000),
+ GEMINI_1_5_FLASH(AiProvider.GEMINI, "gemini-1.5-flash", 1048576),
+ GEMINI_1_5_PRO(AiProvider.GEMINI, "gemini-1.5-pro", 2097152),
+ GEMINI_1_0_PRO(AiProvider.GEMINI, "gemini-1.0-pro", 32000),
+ // Dummy variant for Hugging Face models.
+ HUGGING_FACE(AiProvider.HUGGING_FACE, "", 0);
- public static final Map PROVIDERS_PRIVACY_POLICIES = Map.of(
- AiProvider.OPEN_AI, "https://openai.com/policies/privacy-policy/",
- AiProvider.MISTRAL_AI, "https://mistral.ai/terms/#privacy-policy",
- AiProvider.GEMINI, "https://ai.google.dev/gemini-api/terms",
- AiProvider.HUGGING_FACE, "https://huggingface.co/privacy"
- );
+ private final AiProvider aiProvider;
+ private final String name;
+ private final int contextWindowSize;
- public static final Map PROVIDERS_API_URLS = Map.of(
- AiProvider.OPEN_AI, "https://api.openai.com/v1",
- AiProvider.MISTRAL_AI, "https://api.mistral.ai/v1",
- AiProvider.GEMINI, "https://generativelanguage.googleapis.com/v1beta/",
- AiProvider.HUGGING_FACE, "https://huggingface.co/api"
- );
+ PredefinedChatModel(AiProvider aiProvider, String name, int contextWindowSize) {
+ this.aiProvider = aiProvider;
+ this.name = name;
+ this.contextWindowSize = contextWindowSize;
+ }
- public static final Map> CONTEXT_WINDOW_SIZES = Map.of(
- AiProvider.OPEN_AI, Map.of(
- "gpt-4o-mini", 128000,
- "gpt-4o", 128000,
- "gpt-4", 8192,
- "gpt-4-turbo", 128000,
- "gpt-3.5-turbo", 16385
- ),
- AiProvider.MISTRAL_AI, Map.of(
- "mistral-large-latest", 128000,
- "open-mistral-nemo", 128000,
- "open-mistral-7b", 32000,
- "open-mixtral-8x7b", 32000,
- "open-mixtral-8x22b", 64000
- ),
- AiProvider.GEMINI, Map.of(
- "gemini-1.5-flash", 1048576,
- "gemini-1.5-pro", 2097152,
- "gemini-1.0-pro", 32000
- )
- );
+ public AiProvider getAiProvider() {
+ return aiProvider;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public int getContextWindowSize() {
+ return contextWindowSize;
+ }
+
+ public String toString() {
+ return aiProvider.toString() + " " + name;
+ }
+ }
public static final boolean ENABLE_CHAT = false;
+ public static final boolean AUTO_GENERATE_EMBEDDINGS = false;
+ public static final boolean AUTO_GENERATE_SUMMARIES = false;
public static final AiProvider PROVIDER = AiProvider.OPEN_AI;
- public static final Map CHAT_MODELS = Map.of(
- AiProvider.OPEN_AI, "gpt-4o-mini",
- AiProvider.MISTRAL_AI, "open-mixtral-8x22b",
- AiProvider.GEMINI, "gemini-1.5-flash",
- AiProvider.HUGGING_FACE, ""
+ public static final Map CHAT_MODELS = Map.of(
+ AiProvider.OPEN_AI, PredefinedChatModel.GPT_4O_MINI,
+ AiProvider.MISTRAL_AI, PredefinedChatModel.OPEN_MIXTRAL_8X22B,
+ AiProvider.GEMINI, PredefinedChatModel.GEMINI_1_5_FLASH,
+ AiProvider.HUGGING_FACE, PredefinedChatModel.HUGGING_FACE
);
public static final boolean CUSTOMIZE_SETTINGS = false;
@@ -72,9 +75,20 @@ public class AiDefaultPreferences {
public static final int RAG_MAX_RESULTS_COUNT = 10;
public static final double RAG_MIN_SCORE = 0.3;
- public static final int CONTEXT_WINDOW_SIZE = 8196;
+ public static final int FALLBACK_CONTEXT_WINDOW_SIZE = 8196;
+
+ public static List getAvailableModels(AiProvider aiProvider) {
+ return Arrays.stream(AiDefaultPreferences.PredefinedChatModel.values())
+ .filter(model -> model.getAiProvider() == aiProvider)
+ .map(AiDefaultPreferences.PredefinedChatModel::getName)
+ .toList();
+ }
- public static int getContextWindowSize(AiProvider aiProvider, String model) {
- return CONTEXT_WINDOW_SIZES.getOrDefault(aiProvider, Map.of()).getOrDefault(model, 0);
+ public static int getContextWindowSize(AiProvider aiProvider, String modelName) {
+ return Arrays.stream(AiDefaultPreferences.PredefinedChatModel.values())
+ .filter(model -> model.getAiProvider() == aiProvider && model.getName().equals(modelName))
+ .map(AiDefaultPreferences.PredefinedChatModel::getContextWindowSize)
+ .findFirst()
+ .orElse(AiDefaultPreferences.FALLBACK_CONTEXT_WINDOW_SIZE);
}
}
diff --git a/src/main/java/org/jabref/logic/ai/AiPreferences.java b/src/main/java/org/jabref/logic/ai/AiPreferences.java
index d3c8bb345ae..3a07a02e70c 100644
--- a/src/main/java/org/jabref/logic/ai/AiPreferences.java
+++ b/src/main/java/org/jabref/logic/ai/AiPreferences.java
@@ -31,6 +31,8 @@ public class AiPreferences {
private static final String KEYRING_AI_SERVICE_ACCOUNT = "apiKey";
private final BooleanProperty enableAi;
+ private final BooleanProperty autoGenerateEmbeddings;
+ private final BooleanProperty autoGenerateSummaries;
private final ObjectProperty aiProvider;
@@ -58,6 +60,8 @@ public class AiPreferences {
private Runnable apiKeyChangeListener;
public AiPreferences(boolean enableAi,
+ boolean autoGenerateEmbeddings,
+ boolean autoGenerateSummaries,
AiProvider aiProvider,
String openAiChatModel,
String mistralAiChatModel,
@@ -78,6 +82,8 @@ public AiPreferences(boolean enableAi,
double ragMinScore
) {
this.enableAi = new SimpleBooleanProperty(enableAi);
+ this.autoGenerateEmbeddings = new SimpleBooleanProperty(autoGenerateEmbeddings);
+ this.autoGenerateSummaries = new SimpleBooleanProperty(autoGenerateSummaries);
this.aiProvider = new SimpleObjectProperty<>(aiProvider);
@@ -143,6 +149,30 @@ public void setEnableAi(boolean enableAi) {
this.enableAi.set(enableAi);
}
+ public BooleanProperty autoGenerateEmbeddingsProperty() {
+ return autoGenerateEmbeddings;
+ }
+
+ public boolean getAutoGenerateEmbeddings() {
+ return autoGenerateEmbeddings.get();
+ }
+
+ public void setAutoGenerateEmbeddings(boolean autoGenerateEmbeddings) {
+ this.autoGenerateEmbeddings.set(autoGenerateEmbeddings);
+ }
+
+ public BooleanProperty autoGenerateSummariesProperty() {
+ return autoGenerateSummaries;
+ }
+
+ public boolean getAutoGenerateSummaries() {
+ return autoGenerateSummaries.get();
+ }
+
+ public void setAutoGenerateSummaries(boolean autoGenerateSummaries) {
+ this.autoGenerateSummaries.set(autoGenerateSummaries);
+ }
+
public ObjectProperty aiProviderProperty() {
return aiProvider;
}
@@ -467,7 +497,7 @@ public String getSelectedApiBaseUrl() {
geminiApiBaseUrl.get();
};
} else {
- return AiDefaultPreferences.PROVIDERS_API_URLS.get(aiProvider.get());
+ return aiProvider.get().getApiUrl();
}
}
diff --git a/src/main/java/org/jabref/logic/ai/AiService.java b/src/main/java/org/jabref/logic/ai/AiService.java
index 30655f1721c..a068037b423 100644
--- a/src/main/java/org/jabref/logic/ai/AiService.java
+++ b/src/main/java/org/jabref/logic/ai/AiService.java
@@ -8,6 +8,8 @@
import org.jabref.logic.FilePreferences;
import org.jabref.logic.ai.chatting.AiChatService;
+import org.jabref.logic.ai.chatting.ChatHistoryService;
+import org.jabref.logic.ai.chatting.chathistory.storages.MVStoreChatHistoryStorage;
import org.jabref.logic.ai.chatting.model.JabRefChatLanguageModel;
import org.jabref.logic.ai.ingestion.IngestionService;
import org.jabref.logic.ai.ingestion.MVStoreEmbeddingStore;
@@ -15,9 +17,11 @@
import org.jabref.logic.ai.ingestion.storages.MVStoreFullyIngestedDocumentsTracker;
import org.jabref.logic.ai.summarization.SummariesService;
import org.jabref.logic.ai.summarization.storages.MVStoreSummariesStorage;
+import org.jabref.logic.citationkeypattern.CitationKeyPatternPreferences;
import org.jabref.logic.util.Directories;
import org.jabref.logic.util.NotificationService;
import org.jabref.logic.util.TaskExecutor;
+import org.jabref.model.database.BibDatabaseContext;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
@@ -32,6 +36,7 @@ public class AiService implements AutoCloseable {
private static final String EMBEDDINGS_FILE_NAME = "embeddings.mv";
private static final String FULLY_INGESTED_FILE_NAME = "fully-ingested.mv";
private static final String SUMMARIES_FILE_NAME = "summaries.mv";
+ private static final String CHAT_HISTORY_FILE_NAME = "chat-histories.mv";
// This field is used to shut down AI-related background tasks.
// If a background task processes a big document and has a loop, then the task should check the status
@@ -42,10 +47,12 @@ public class AiService implements AutoCloseable {
new ThreadFactoryBuilder().setNameFormat("ai-retrieval-pool-%d").build()
);
+ private final MVStoreChatHistoryStorage mvStoreChatHistoryStorage;
private final MVStoreEmbeddingStore mvStoreEmbeddingStore;
private final MVStoreFullyIngestedDocumentsTracker mvStoreFullyIngestedDocumentsTracker;
private final MVStoreSummariesStorage mvStoreSummariesStorage;
+ private final ChatHistoryService chatHistoryService;
private final JabRefChatLanguageModel jabRefChatLanguageModel;
private final JabRefEmbeddingModel jabRefEmbeddingModel;
private final AiChatService aiChatService;
@@ -54,17 +61,22 @@ public class AiService implements AutoCloseable {
public AiService(AiPreferences aiPreferences,
FilePreferences filePreferences,
+ CitationKeyPatternPreferences citationKeyPatternPreferences,
NotificationService notificationService,
TaskExecutor taskExecutor
) {
- this.jabRefChatLanguageModel = new JabRefChatLanguageModel(aiPreferences);
+ this.mvStoreChatHistoryStorage = new MVStoreChatHistoryStorage(Directories.getAiFilesDirectory().resolve(CHAT_HISTORY_FILE_NAME), notificationService);
this.mvStoreEmbeddingStore = new MVStoreEmbeddingStore(Directories.getAiFilesDirectory().resolve(EMBEDDINGS_FILE_NAME), notificationService);
this.mvStoreFullyIngestedDocumentsTracker = new MVStoreFullyIngestedDocumentsTracker(Directories.getAiFilesDirectory().resolve(FULLY_INGESTED_FILE_NAME), notificationService);
this.mvStoreSummariesStorage = new MVStoreSummariesStorage(Directories.getAiFilesDirectory().resolve(SUMMARIES_FILE_NAME), notificationService);
+ this.chatHistoryService = new ChatHistoryService(citationKeyPatternPreferences, mvStoreChatHistoryStorage);
+ this.jabRefChatLanguageModel = new JabRefChatLanguageModel(aiPreferences);
this.jabRefEmbeddingModel = new JabRefEmbeddingModel(aiPreferences, notificationService, taskExecutor);
+
this.aiChatService = new AiChatService(aiPreferences, jabRefChatLanguageModel, jabRefEmbeddingModel, mvStoreEmbeddingStore, cachedThreadPool);
+
this.ingestionService = new IngestionService(
aiPreferences,
shutdownSignal,
@@ -74,7 +86,15 @@ public AiService(AiPreferences aiPreferences,
filePreferences,
taskExecutor
);
- this.summariesService = new SummariesService(aiPreferences, mvStoreSummariesStorage, jabRefChatLanguageModel, shutdownSignal, filePreferences, taskExecutor);
+
+ this.summariesService = new SummariesService(
+ aiPreferences,
+ mvStoreSummariesStorage,
+ jabRefChatLanguageModel,
+ shutdownSignal,
+ filePreferences,
+ taskExecutor
+ );
}
public JabRefChatLanguageModel getChatLanguageModel() {
@@ -85,6 +105,10 @@ public JabRefEmbeddingModel getEmbeddingModel() {
return jabRefEmbeddingModel;
}
+ public ChatHistoryService getChatHistoryService() {
+ return chatHistoryService;
+ }
+
public AiChatService getAiChatService() {
return aiChatService;
}
@@ -97,6 +121,12 @@ public SummariesService getSummariesService() {
return summariesService;
}
+ public void setupDatabase(BibDatabaseContext context) {
+ chatHistoryService.setupDatabase(context);
+ ingestionService.setupDatabase(context);
+ summariesService.setupDatabase(context);
+ }
+
@Override
public void close() {
shutdownSignal.set(true);
diff --git a/src/main/java/org/jabref/logic/ai/chatting/AiChatLogic.java b/src/main/java/org/jabref/logic/ai/chatting/AiChatLogic.java
index 8a802847d5b..f079b093604 100644
--- a/src/main/java/org/jabref/logic/ai/chatting/AiChatLogic.java
+++ b/src/main/java/org/jabref/logic/ai/chatting/AiChatLogic.java
@@ -8,7 +8,6 @@
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
-import org.jabref.logic.ai.AiDefaultPreferences;
import org.jabref.logic.ai.AiPreferences;
import org.jabref.logic.ai.ingestion.FileEmbeddingsManager;
import org.jabref.logic.ai.util.ErrorMessage;
@@ -160,7 +159,7 @@ public AiMessage execute(UserMessage message) {
// Message will be automatically added to ChatMemory through ConversationalRetrievalChain.
LOGGER.info("Sending message to AI provider ({}) for answering in {}: {}",
- AiDefaultPreferences.PROVIDERS_API_URLS.get(aiPreferences.getAiProvider()),
+ aiPreferences.getAiProvider().getApiUrl(),
name.get(),
message.singleText());
diff --git a/src/main/java/org/jabref/gui/ai/chatting/chathistory/ChatHistoryService.java b/src/main/java/org/jabref/logic/ai/chatting/ChatHistoryService.java
similarity index 77%
rename from src/main/java/org/jabref/gui/ai/chatting/chathistory/ChatHistoryService.java
rename to src/main/java/org/jabref/logic/ai/chatting/ChatHistoryService.java
index 872a263c7a1..cacf18d6e36 100644
--- a/src/main/java/org/jabref/gui/ai/chatting/chathistory/ChatHistoryService.java
+++ b/src/main/java/org/jabref/logic/ai/chatting/ChatHistoryService.java
@@ -1,4 +1,4 @@
-package org.jabref.gui.ai.chatting.chathistory;
+package org.jabref.logic.ai.chatting;
import java.util.Comparator;
import java.util.HashSet;
@@ -7,17 +7,13 @@
import java.util.TreeMap;
import javafx.collections.FXCollections;
-import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import org.jabref.gui.StateManager;
import org.jabref.logic.ai.chatting.chathistory.ChatHistoryStorage;
-import org.jabref.logic.ai.chatting.chathistory.storages.MVStoreChatHistoryStorage;
import org.jabref.logic.ai.util.CitationKeyCheck;
import org.jabref.logic.citationkeypattern.CitationKeyGenerator;
import org.jabref.logic.citationkeypattern.CitationKeyPatternPreferences;
-import org.jabref.logic.util.Directories;
-import org.jabref.logic.util.NotificationService;
import org.jabref.model.database.BibDatabaseContext;
import org.jabref.model.entry.BibEntry;
import org.jabref.model.entry.event.FieldChangedEvent;
@@ -25,7 +21,6 @@
import org.jabref.model.groups.AbstractGroup;
import org.jabref.model.groups.GroupTreeNode;
-import com.airhacks.afterburner.injection.Injector;
import com.google.common.eventbus.Subscribe;
import dev.langchain4j.data.message.ChatMessage;
import org.slf4j.Logger;
@@ -56,14 +51,12 @@
public class ChatHistoryService implements AutoCloseable {
private static final Logger LOGGER = LoggerFactory.getLogger(ChatHistoryService.class);
- private static final String CHAT_HISTORY_FILE_NAME = "chat-histories.mv";
-
- private final StateManager stateManager = Injector.instantiateModelOrService(StateManager.class);
-
private final CitationKeyPatternPreferences citationKeyPatternPreferences;
-
private final ChatHistoryStorage implementation;
+ // Note about `Optional`: it was necessary in previous version, but currently we never save an `Optional.empty()`.
+ // However, we decided to left it here: to reduce migrations and to make possible to chat with a {@link BibEntry} without {@link BibDatabaseContext}
+ // ({@link BibDatabaseContext} is required only for load/store of the chat).
private record ChatHistoryManagementRecord(Optional bibDatabaseContext, ObservableList chatHistory) { }
// We use a {@link TreeMap} here to store {@link BibEntry} chat histories by their id.
@@ -73,37 +66,14 @@ private record ChatHistoryManagementRecord(Optional bibDatab
private final TreeMap bibEntriesChatHistory = new TreeMap<>(Comparator.comparing(BibEntry::getId));
// We use {@link TreeMap} for group chat history for the same reason as for {@link BibEntry}ies.
- private final TreeMap groupsChatHistory = new TreeMap<>((o1, o2) -> {
- // The most important thing is to catch equality/non-equality.
- // For "less" or "bigger" comparison, we will fall back to group names.
- return o1 == o2 ? 0 : o1.getGroup().getName().compareTo(o2.getGroup().getName());
- });
+ private final TreeMap groupsChatHistory = new TreeMap<>(Comparator.comparing(GroupTreeNode::getName));
- public ChatHistoryService(CitationKeyPatternPreferences citationKeyPatternPreferences, NotificationService notificationService) {
- this.citationKeyPatternPreferences = citationKeyPatternPreferences;
- this.implementation = new MVStoreChatHistoryStorage(Directories.getAiFilesDirectory().resolve(CHAT_HISTORY_FILE_NAME), notificationService);
- configureHistoryTransfer();
- }
-
- public ChatHistoryService(CitationKeyPatternPreferences citationKeyPatternPreferences,
- ChatHistoryStorage implementation) {
+ public ChatHistoryService(CitationKeyPatternPreferences citationKeyPatternPreferences, ChatHistoryStorage implementation) {
this.citationKeyPatternPreferences = citationKeyPatternPreferences;
this.implementation = implementation;
-
- configureHistoryTransfer();
}
- private void configureHistoryTransfer() {
- stateManager.getOpenDatabases().addListener((ListChangeListener) change -> {
- while (change.next()) {
- if (change.wasAdded()) {
- change.getAddedSubList().forEach(this::configureHistoryTransfer);
- }
- }
- });
- }
-
- private void configureHistoryTransfer(BibDatabaseContext bibDatabaseContext) {
+ public void setupDatabase(BibDatabaseContext bibDatabaseContext) {
bibDatabaseContext.getMetaData().getGroups().ifPresent(rootGroupTreeNode -> {
rootGroupTreeNode.iterateOverTree().forEach(groupNode -> {
groupNode.getGroup().nameProperty().addListener((observable, oldValue, newValue) -> {
@@ -125,20 +95,18 @@ private void configureHistoryTransfer(BibDatabaseContext bibDatabaseContext) {
});
}
- public ObservableList getChatHistoryForEntry(BibEntry entry) {
+ public ObservableList getChatHistoryForEntry(BibDatabaseContext bibDatabaseContext, BibEntry entry) {
return bibEntriesChatHistory.computeIfAbsent(entry, entryArg -> {
- Optional bibDatabaseContext = findBibDatabaseForEntry(entry);
-
ObservableList chatHistory;
- if (bibDatabaseContext.isEmpty() || entry.getCitationKey().isEmpty() || !correctCitationKey(bibDatabaseContext.get(), entry) || bibDatabaseContext.get().getDatabasePath().isEmpty()) {
+ if (entry.getCitationKey().isEmpty() || !correctCitationKey(bibDatabaseContext, entry) || bibDatabaseContext.getDatabasePath().isEmpty()) {
chatHistory = FXCollections.observableArrayList();
} else {
- List chatMessagesList = implementation.loadMessagesForEntry(bibDatabaseContext.get().getDatabasePath().get(), entry.getCitationKey().get());
+ List chatMessagesList = implementation.loadMessagesForEntry(bibDatabaseContext.getDatabasePath().get(), entry.getCitationKey().get());
chatHistory = FXCollections.observableArrayList(chatMessagesList);
}
- return new ChatHistoryManagementRecord(bibDatabaseContext, chatHistory);
+ return new ChatHistoryManagementRecord(Optional.of(bibDatabaseContext), chatHistory);
}).chatHistory;
}
@@ -173,24 +141,22 @@ public void closeChatHistoryForEntry(BibEntry entry) {
bibEntriesChatHistory.remove(entry);
}
- public ObservableList getChatHistoryForGroup(GroupTreeNode group) {
+ public ObservableList getChatHistoryForGroup(BibDatabaseContext bibDatabaseContext, GroupTreeNode group) {
return groupsChatHistory.computeIfAbsent(group, groupArg -> {
- Optional bibDatabaseContext = findBibDatabaseForGroup(group);
-
ObservableList chatHistory;
- if (bibDatabaseContext.isEmpty() || bibDatabaseContext.get().getDatabasePath().isEmpty()) {
+ if (bibDatabaseContext.getDatabasePath().isEmpty()) {
chatHistory = FXCollections.observableArrayList();
} else {
List chatMessagesList = implementation.loadMessagesForGroup(
- bibDatabaseContext.get().getDatabasePath().get(),
+ bibDatabaseContext.getDatabasePath().get(),
group.getGroup().getName()
);
chatHistory = FXCollections.observableArrayList(chatMessagesList);
}
- return new ChatHistoryManagementRecord(bibDatabaseContext, chatHistory);
+ return new ChatHistoryManagementRecord(Optional.of(bibDatabaseContext), chatHistory);
}).chatHistory;
}
@@ -235,26 +201,6 @@ private void tryToGenerateCitationKey(BibDatabaseContext bibDatabaseContext, Bib
new CitationKeyGenerator(bibDatabaseContext, citationKeyPatternPreferences).generateAndSetKey(bibEntry);
}
- private Optional findBibDatabaseForEntry(BibEntry entry) {
- return stateManager
- .getOpenDatabases()
- .stream()
- .filter(dbContext -> dbContext.getDatabase().getEntries().contains(entry))
- .findFirst();
- }
-
- private Optional findBibDatabaseForGroup(GroupTreeNode group) {
- return stateManager
- .getOpenDatabases()
- .stream()
- .filter(dbContext ->
- dbContext.getMetaData().groupsBinding().get().map(groupTreeNode ->
- groupTreeNode.containsGroup(group.getGroup())
- ).orElse(false)
- )
- .findFirst();
- }
-
@Override
public void close() {
// We need to clone `bibEntriesChatHistory.keySet()` because closeChatHistoryForEntry() modifies the `bibEntriesChatHistory` map.
@@ -264,6 +210,7 @@ public void close() {
new HashSet<>(groupsChatHistory.keySet()).forEach(this::closeChatHistoryForGroup);
implementation.commit();
+ implementation.close();
}
private void transferGroupHistory(BibDatabaseContext bibDatabaseContext, GroupTreeNode groupTreeNode, String oldName, String newName) {
diff --git a/src/main/java/org/jabref/logic/ai/ingestion/GenerateEmbeddingsForSeveralTask.java b/src/main/java/org/jabref/logic/ai/ingestion/GenerateEmbeddingsForSeveralTask.java
index 0bb1626a035..6427a08de49 100644
--- a/src/main/java/org/jabref/logic/ai/ingestion/GenerateEmbeddingsForSeveralTask.java
+++ b/src/main/java/org/jabref/logic/ai/ingestion/GenerateEmbeddingsForSeveralTask.java
@@ -29,7 +29,7 @@
public class GenerateEmbeddingsForSeveralTask extends BackgroundTask {
private static final Logger LOGGER = LoggerFactory.getLogger(GenerateEmbeddingsForSeveralTask.class);
- private final StringProperty name;
+ private final StringProperty groupName;
private final List> linkedFiles;
private final FileEmbeddingsManager fileEmbeddingsManager;
private final BibDatabaseContext bibDatabaseContext;
@@ -42,7 +42,7 @@ public class GenerateEmbeddingsForSeveralTask extends BackgroundTask {
private String currentFile = "";
public GenerateEmbeddingsForSeveralTask(
- StringProperty name,
+ StringProperty groupName,
List> linkedFiles,
FileEmbeddingsManager fileEmbeddingsManager,
BibDatabaseContext bibDatabaseContext,
@@ -50,7 +50,7 @@ public GenerateEmbeddingsForSeveralTask(
TaskExecutor taskExecutor,
ReadOnlyBooleanProperty shutdownSignal
) {
- this.name = name;
+ this.groupName = groupName;
this.linkedFiles = linkedFiles;
this.fileEmbeddingsManager = fileEmbeddingsManager;
this.bibDatabaseContext = bibDatabaseContext;
@@ -58,7 +58,7 @@ public GenerateEmbeddingsForSeveralTask(
this.taskExecutor = taskExecutor;
this.shutdownSignal = shutdownSignal;
- configure(name);
+ configure(groupName);
}
private void configure(StringProperty name) {
@@ -73,9 +73,10 @@ private void configure(StringProperty name) {
@Override
public Void call() throws Exception {
- LOGGER.debug("Starting embeddings generation of several files for {}", name.get());
+ LOGGER.debug("Starting embeddings generation of several files for {}", groupName.get());
List, String>> futures = new ArrayList<>();
+
linkedFiles
.stream()
.map(processingInfo -> {
@@ -88,6 +89,7 @@ public Void call() throws Exception {
filePreferences,
shutdownSignal
)
+ .showToUser(false)
.onSuccess(v -> processingInfo.setState(ProcessingState.SUCCESS))
.onFailure(processingInfo::setException)
.onFinished(() -> progressCounter.increaseWorkDone(1))
@@ -101,7 +103,7 @@ public Void call() throws Exception {
pair.getKey().get();
}
- LOGGER.debug("Finished embeddings generation task of several files for {}", name.get());
+ LOGGER.debug("Finished embeddings generation task of several files for {}", groupName.get());
progressCounter.stop();
return null;
}
diff --git a/src/main/java/org/jabref/logic/ai/ingestion/GenerateEmbeddingsTask.java b/src/main/java/org/jabref/logic/ai/ingestion/GenerateEmbeddingsTask.java
index 11bf406ca55..219217be8bc 100644
--- a/src/main/java/org/jabref/logic/ai/ingestion/GenerateEmbeddingsTask.java
+++ b/src/main/java/org/jabref/logic/ai/ingestion/GenerateEmbeddingsTask.java
@@ -48,10 +48,11 @@ public GenerateEmbeddingsTask(LinkedFile linkedFile,
this.filePreferences = filePreferences;
this.shutdownSignal = shutdownSignal;
- configure(linkedFile);
+ configure();
}
- private void configure(LinkedFile linkedFile) {
+ private void configure() {
+ showToUser(true);
titleProperty().set(Localization.lang("Generating embeddings for file '%0'", linkedFile.getLink()));
progressCounter.listenToAllProperties(this::updateProgress);
diff --git a/src/main/java/org/jabref/logic/ai/ingestion/IngestionService.java b/src/main/java/org/jabref/logic/ai/ingestion/IngestionService.java
index 6d113891a36..fd5812cfe7e 100644
--- a/src/main/java/org/jabref/logic/ai/ingestion/IngestionService.java
+++ b/src/main/java/org/jabref/logic/ai/ingestion/IngestionService.java
@@ -1,9 +1,9 @@
package org.jabref.logic.ai.ingestion;
import java.util.ArrayList;
-import java.util.HashMap;
+import java.util.Comparator;
import java.util.List;
-import java.util.Map;
+import java.util.TreeMap;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.StringProperty;
@@ -14,8 +14,12 @@
import org.jabref.logic.ai.processingstatus.ProcessingState;
import org.jabref.logic.util.TaskExecutor;
import org.jabref.model.database.BibDatabaseContext;
+import org.jabref.model.database.event.EntriesAddedEvent;
import org.jabref.model.entry.LinkedFile;
+import org.jabref.model.entry.event.FieldChangedEvent;
+import org.jabref.model.entry.field.StandardField;
+import com.google.common.eventbus.Subscribe;
import dev.langchain4j.data.segment.TextSegment;
import dev.langchain4j.model.embedding.EmbeddingModel;
import dev.langchain4j.store.embedding.EmbeddingStore;
@@ -25,10 +29,12 @@
* Use this class in the logic and UI.
*/
public class IngestionService {
- private final Map> ingestionStatusMap = new HashMap<>();
+ // We use a {@link TreeMap} here for the same reasons we use it in {@link ChatHistoryService}.
+ private final TreeMap> ingestionStatusMap = new TreeMap<>(Comparator.comparing(LinkedFile::getLink));
private final List> listsUnderIngestion = new ArrayList<>();
+ private final AiPreferences aiPreferences;
private final FilePreferences filePreferences;
private final TaskExecutor taskExecutor;
@@ -44,6 +50,7 @@ public IngestionService(AiPreferences aiPreferences,
FilePreferences filePreferences,
TaskExecutor taskExecutor
) {
+ this.aiPreferences = aiPreferences;
this.filePreferences = filePreferences;
this.taskExecutor = taskExecutor;
@@ -58,6 +65,37 @@ public IngestionService(AiPreferences aiPreferences,
this.shutdownSignal = shutdownSignal;
}
+ public void setupDatabase(BibDatabaseContext bibDatabaseContext) {
+ // GC was eating the listeners, so we have to fall back to the event bus.
+ bibDatabaseContext.getDatabase().registerListener(new EntriesChangedListener(bibDatabaseContext));
+ }
+
+ private class EntriesChangedListener {
+ private final BibDatabaseContext bibDatabaseContext;
+
+ public EntriesChangedListener(BibDatabaseContext bibDatabaseContext) {
+ this.bibDatabaseContext = bibDatabaseContext;
+ }
+
+ @Subscribe
+ public void listen(EntriesAddedEvent e) {
+ e.getBibEntries().forEach(entry -> {
+ if (aiPreferences.getAutoGenerateEmbeddings()) {
+ entry.getFiles().forEach(linkedFile -> ingest(linkedFile, bibDatabaseContext));
+ }
+
+ entry.registerListener(this);
+ });
+ }
+
+ @Subscribe
+ public void listen(FieldChangedEvent e) {
+ if (e.getField() == StandardField.FILE && aiPreferences.getAutoGenerateEmbeddings()) {
+ e.getBibEntry().getFiles().forEach(linkedFile -> ingest(linkedFile, bibDatabaseContext));
+ }
+ }
+ }
+
/**
* Start ingesting of a {@link LinkedFile}, if it was not ingested.
* This method returns a {@link ProcessingInfo} that can be used for tracking state of the ingestion.
@@ -86,28 +124,35 @@ public List> getProcessingInfo(List
return linkedFiles.stream().map(this::getProcessingInfo).toList();
}
- public List> ingest(StringProperty name, List linkedFiles, BibDatabaseContext bibDatabaseContext) {
+ public List> ingest(StringProperty groupName, List linkedFiles, BibDatabaseContext bibDatabaseContext) {
List> result = getProcessingInfo(linkedFiles);
if (listsUnderIngestion.contains(linkedFiles)) {
return result;
}
+ listsUnderIngestion.add(linkedFiles);
+
List> needToProcess = result.stream().filter(processingInfo -> processingInfo.getState() == ProcessingState.STOPPED).toList();
- startEmbeddingsGenerationTask(name, needToProcess, bibDatabaseContext);
+ startEmbeddingsGenerationTask(groupName, needToProcess, bibDatabaseContext);
return result;
}
private void startEmbeddingsGenerationTask(LinkedFile linkedFile, BibDatabaseContext bibDatabaseContext, ProcessingInfo processingInfo) {
+ processingInfo.setState(ProcessingState.PROCESSING);
+
new GenerateEmbeddingsTask(linkedFile, fileEmbeddingsManager, bibDatabaseContext, filePreferences, shutdownSignal)
+ .showToUser(true)
.onSuccess(v -> processingInfo.setState(ProcessingState.SUCCESS))
.onFailure(processingInfo::setException)
.executeWith(taskExecutor);
}
- private void startEmbeddingsGenerationTask(StringProperty name, List> linkedFiles, BibDatabaseContext bibDatabaseContext) {
- new GenerateEmbeddingsForSeveralTask(name, linkedFiles, fileEmbeddingsManager, bibDatabaseContext, filePreferences, taskExecutor, shutdownSignal)
+ private void startEmbeddingsGenerationTask(StringProperty groupName, List> linkedFiles, BibDatabaseContext bibDatabaseContext) {
+ linkedFiles.forEach(processingInfo -> processingInfo.setState(ProcessingState.PROCESSING));
+
+ new GenerateEmbeddingsForSeveralTask(groupName, linkedFiles, fileEmbeddingsManager, bibDatabaseContext, filePreferences, taskExecutor, shutdownSignal)
.executeWith(taskExecutor);
}
diff --git a/src/main/java/org/jabref/logic/ai/summarization/GenerateSummaryForSeveralTask.java b/src/main/java/org/jabref/logic/ai/summarization/GenerateSummaryForSeveralTask.java
new file mode 100644
index 00000000000..4940fba8b21
--- /dev/null
+++ b/src/main/java/org/jabref/logic/ai/summarization/GenerateSummaryForSeveralTask.java
@@ -0,0 +1,125 @@
+package org.jabref.logic.ai.summarization;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Future;
+
+import javafx.beans.property.ReadOnlyBooleanProperty;
+import javafx.beans.property.StringProperty;
+import javafx.util.Pair;
+
+import org.jabref.logic.FilePreferences;
+import org.jabref.logic.ai.AiPreferences;
+import org.jabref.logic.ai.processingstatus.ProcessingInfo;
+import org.jabref.logic.ai.processingstatus.ProcessingState;
+import org.jabref.logic.l10n.Localization;
+import org.jabref.logic.util.BackgroundTask;
+import org.jabref.logic.util.ProgressCounter;
+import org.jabref.logic.util.TaskExecutor;
+import org.jabref.model.database.BibDatabaseContext;
+import org.jabref.model.entry.BibEntry;
+
+import dev.langchain4j.model.chat.ChatLanguageModel;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This task generates summaries for several {@link BibEntry}ies (typically used for groups).
+ * It will check if summaries were already generated.
+ * And it also will store the summaries.
+ */
+public class GenerateSummaryForSeveralTask extends BackgroundTask {
+ private static final Logger LOGGER = LoggerFactory.getLogger(GenerateSummaryForSeveralTask.class);
+
+ private final StringProperty groupName;
+ private final List> entries;
+ private final BibDatabaseContext bibDatabaseContext;
+ private final SummariesStorage summariesStorage;
+ private final ChatLanguageModel chatLanguageModel;
+ private final ReadOnlyBooleanProperty shutdownSignal;
+ private final AiPreferences aiPreferences;
+ private final FilePreferences filePreferences;
+ private final TaskExecutor taskExecutor;
+
+ private final ProgressCounter progressCounter = new ProgressCounter();
+
+ private String currentFile = "";
+
+ public GenerateSummaryForSeveralTask(
+ StringProperty groupName,
+ List> entries,
+ BibDatabaseContext bibDatabaseContext,
+ SummariesStorage summariesStorage,
+ ChatLanguageModel chatLanguageModel,
+ ReadOnlyBooleanProperty shutdownSignal,
+ AiPreferences aiPreferences,
+ FilePreferences filePreferences,
+ TaskExecutor taskExecutor
+ ) {
+ this.groupName = groupName;
+ this.entries = entries;
+ this.bibDatabaseContext = bibDatabaseContext;
+ this.summariesStorage = summariesStorage;
+ this.chatLanguageModel = chatLanguageModel;
+ this.shutdownSignal = shutdownSignal;
+ this.aiPreferences = aiPreferences;
+ this.filePreferences = filePreferences;
+ this.taskExecutor = taskExecutor;
+
+ configure();
+ }
+
+ private void configure() {
+ showToUser(true);
+ titleProperty().set(Localization.lang("Generating summaries for %0", groupName.get()));
+ groupName.addListener((o, oldValue, newValue) -> titleProperty().set(Localization.lang("Generating summaries for %0", newValue)));
+
+ progressCounter.increaseWorkMax(entries.size());
+ progressCounter.listenToAllProperties(this::updateProgress);
+ updateProgress();
+ }
+
+ @Override
+ public Void call() throws Exception {
+ LOGGER.debug("Starting summaries generation of several files for {}", groupName.get());
+
+ List, BibEntry>> futures = new ArrayList<>();
+
+ entries
+ .stream()
+ .map(processingInfo -> {
+ processingInfo.setState(ProcessingState.PROCESSING);
+ return new Pair<>(
+ new GenerateSummaryTask(
+ processingInfo.getObject(),
+ bibDatabaseContext,
+ summariesStorage,
+ chatLanguageModel,
+ shutdownSignal,
+ aiPreferences,
+ filePreferences
+ )
+ .showToUser(false)
+ .onSuccess(processingInfo::setSuccess)
+ .onFailure(processingInfo::setException)
+ .onFinished(() -> progressCounter.increaseWorkDone(1))
+ .executeWith(taskExecutor),
+ processingInfo.getObject());
+ })
+ .forEach(futures::add);
+
+ for (Pair extends Future>, BibEntry> pair : futures) {
+ currentFile = pair.getValue().getCitationKey().orElse("");
+ pair.getKey().get();
+ }
+
+ LOGGER.debug("Finished embeddings generation task of several files for {}", groupName.get());
+ progressCounter.stop();
+ return null;
+ }
+
+ private void updateProgress() {
+ updateProgress(progressCounter.getWorkDone(), progressCounter.getWorkMax());
+ updateMessage(progressCounter.getMessage() + " - " + currentFile + ", ...");
+ }
+}
diff --git a/src/main/java/org/jabref/logic/ai/summarization/GenerateSummaryTask.java b/src/main/java/org/jabref/logic/ai/summarization/GenerateSummaryTask.java
index 208de09f490..f863c8f01e0 100644
--- a/src/main/java/org/jabref/logic/ai/summarization/GenerateSummaryTask.java
+++ b/src/main/java/org/jabref/logic/ai/summarization/GenerateSummaryTask.java
@@ -1,6 +1,7 @@
package org.jabref.logic.ai.summarization;
import java.nio.file.Path;
+import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -8,15 +9,17 @@
import java.util.stream.Collectors;
import java.util.stream.Stream;
-import javafx.beans.property.BooleanProperty;
+import javafx.beans.property.ReadOnlyBooleanProperty;
import org.jabref.logic.FilePreferences;
import org.jabref.logic.ai.AiPreferences;
import org.jabref.logic.ai.ingestion.FileToDocument;
+import org.jabref.logic.ai.util.CitationKeyCheck;
import org.jabref.logic.l10n.Localization;
import org.jabref.logic.util.BackgroundTask;
import org.jabref.logic.util.ProgressCounter;
import org.jabref.model.database.BibDatabaseContext;
+import org.jabref.model.entry.BibEntry;
import org.jabref.model.entry.LinkedFile;
import dev.langchain4j.data.document.Document;
@@ -31,12 +34,12 @@
/**
* This task generates a new summary for an entry.
- * It will not check if summary was already generated.
- * And it also does not store the summary.
+ * It will check if summary was already generated.
+ * And it also will store the summary.
*
* This task is created in the {@link SummariesService}, and stored then in a {@link SummariesStorage}.
*/
-public class GenerateSummaryTask extends BackgroundTask {
+public class GenerateSummaryTask extends BackgroundTask {
private static final Logger LOGGER = LoggerFactory.getLogger(GenerateSummaryTask.class);
// Be careful when constructing prompt.
@@ -69,57 +72,89 @@ public class GenerateSummaryTask extends BackgroundTask {
private static final int CHAR_TOKEN_FACTOR = 4; // Means, every token is roughly 4 characters.
private final BibDatabaseContext bibDatabaseContext;
+ private final BibEntry entry;
private final String citationKey;
- private final List linkedFiles;
private final ChatLanguageModel chatLanguageModel;
- private final BooleanProperty shutdownSignal;
+ private final SummariesStorage summariesStorage;
+ private final ReadOnlyBooleanProperty shutdownSignal;
private final AiPreferences aiPreferences;
private final FilePreferences filePreferences;
private final ProgressCounter progressCounter = new ProgressCounter();
- public GenerateSummaryTask(BibDatabaseContext bibDatabaseContext,
- String citationKey,
- List linkedFiles,
+ public GenerateSummaryTask(BibEntry entry,
+ BibDatabaseContext bibDatabaseContext,
+ SummariesStorage summariesStorage,
ChatLanguageModel chatLanguageModel,
- BooleanProperty shutdownSignal,
+ ReadOnlyBooleanProperty shutdownSignal,
AiPreferences aiPreferences,
FilePreferences filePreferences
) {
this.bibDatabaseContext = bibDatabaseContext;
- this.citationKey = citationKey;
- this.linkedFiles = linkedFiles;
+ this.entry = entry;
+ this.citationKey = entry.getCitationKey().orElse("");
this.chatLanguageModel = chatLanguageModel;
+ this.summariesStorage = summariesStorage;
this.shutdownSignal = shutdownSignal;
this.aiPreferences = aiPreferences;
this.filePreferences = filePreferences;
- configure(citationKey);
+ configure();
}
- private void configure(String citationKey) {
- titleProperty().set(Localization.lang("Waiting summary for %0...", citationKey));
+ private void configure() {
showToUser(true);
+ titleProperty().set(Localization.lang("Waiting summary for %0...", citationKey));
progressCounter.listenToAllProperties(this::updateProgress);
}
@Override
- public String call() throws Exception {
+ public Summary call() throws Exception {
LOGGER.debug("Starting summarization task for entry {}", citationKey);
- String result = null;
+ Optional savedSummary = Optional.empty();
- try {
- result = summarizeAll();
- } catch (InterruptedException e) {
- LOGGER.debug("There was a summarization task for {}. It will be canceled, because user quits JabRef.", citationKey);
+ if (bibDatabaseContext.getDatabasePath().isEmpty()) {
+ LOGGER.info("No database path is present. Summary will not be stored in the next sessions");
+ } else if (entry.getCitationKey().isEmpty()) {
+ LOGGER.info("No citation key is present. Summary will not be stored in the next sessions");
+ } else {
+ savedSummary = summariesStorage.get(bibDatabaseContext.getDatabasePath().get(), entry.getCitationKey().get());
+ }
+
+ Summary summary;
+
+ if (savedSummary.isPresent()) {
+ summary = savedSummary.get();
+ } else {
+ try {
+ String result = summarizeAll();
+
+ summary = new Summary(
+ LocalDateTime.now(),
+ aiPreferences.getAiProvider(),
+ aiPreferences.getSelectedChatModel(),
+ result
+ );
+ } catch (InterruptedException e) {
+ LOGGER.debug("There was a summarization task for {}. It will be canceled, because user quits JabRef.", citationKey);
+ return null;
+ }
+ }
+
+ if (bibDatabaseContext.getDatabasePath().isEmpty()) {
+ LOGGER.info("No database path is present. Summary will not be stored in the next sessions");
+ } else if (CitationKeyCheck.citationKeyIsPresentAndUnique(bibDatabaseContext, entry)) {
+ LOGGER.info("No valid citation key is present. Summary will not be stored in the next sessions");
+ } else {
+ summariesStorage.set(bibDatabaseContext.getDatabasePath().get(), entry.getCitationKey().get(), summary);
}
- showToUser(false);
LOGGER.debug("Finished summarization task for entry {}", citationKey);
progressCounter.stop();
- return result;
+
+ return summary;
}
private String summarizeAll() throws InterruptedException {
@@ -129,7 +164,7 @@ private String summarizeAll() throws InterruptedException {
// Stream API would look better here, but we need to catch InterruptedException.
List linkedFilesSummary = new ArrayList<>();
- for (LinkedFile linkedFile : linkedFiles) {
+ for (LinkedFile linkedFile : entry.getFiles()) {
Optional s = generateSummary(linkedFile);
if (s.isPresent()) {
String string = s.get();
diff --git a/src/main/java/org/jabref/logic/ai/summarization/SummariesService.java b/src/main/java/org/jabref/logic/ai/summarization/SummariesService.java
index c71db69cdf8..afac5e823f5 100644
--- a/src/main/java/org/jabref/logic/ai/summarization/SummariesService.java
+++ b/src/main/java/org/jabref/logic/ai/summarization/SummariesService.java
@@ -1,11 +1,12 @@
package org.jabref.logic.ai.summarization;
-import java.time.LocalDateTime;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Optional;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.TreeMap;
import javafx.beans.property.BooleanProperty;
+import javafx.beans.property.StringProperty;
import org.jabref.logic.FilePreferences;
import org.jabref.logic.ai.AiPreferences;
@@ -14,8 +15,12 @@
import org.jabref.logic.ai.util.CitationKeyCheck;
import org.jabref.logic.util.TaskExecutor;
import org.jabref.model.database.BibDatabaseContext;
+import org.jabref.model.database.event.EntriesAddedEvent;
import org.jabref.model.entry.BibEntry;
+import org.jabref.model.entry.event.FieldChangedEvent;
+import org.jabref.model.entry.field.StandardField;
+import com.google.common.eventbus.Subscribe;
import dev.langchain4j.model.chat.ChatLanguageModel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -32,7 +37,9 @@
public class SummariesService {
private static final Logger LOGGER = LoggerFactory.getLogger(SummariesService.class);
- private final Map> summariesStatusMap = new HashMap<>();
+ private final TreeMap> summariesStatusMap = new TreeMap<>(Comparator.comparing(BibEntry::getId));
+
+ private final List> listsUnderSummarization = new ArrayList<>();
private final AiPreferences aiPreferences;
private final SummariesStorage summariesStorage;
@@ -56,6 +63,35 @@ public SummariesService(AiPreferences aiPreferences,
this.taskExecutor = taskExecutor;
}
+ public void setupDatabase(BibDatabaseContext bibDatabaseContext) {
+ // GC was eating the listeners, so we have to fall back to the event bus.
+ bibDatabaseContext.getDatabase().registerListener(new EntriesChangedListener(bibDatabaseContext));
+ }
+
+ private class EntriesChangedListener {
+ private final BibDatabaseContext bibDatabaseContext;
+
+ public EntriesChangedListener(BibDatabaseContext bibDatabaseContext) {
+ this.bibDatabaseContext = bibDatabaseContext;
+ }
+
+ @Subscribe
+ public void listen(EntriesAddedEvent e) {
+ e.getBibEntries().forEach(entry -> {
+ if (aiPreferences.getAutoGenerateSummaries()) {
+ summarize(entry, bibDatabaseContext);
+ }
+ });
+ }
+
+ @Subscribe
+ public void listen(FieldChangedEvent e) {
+ if (e.getField() == StandardField.FILE && aiPreferences.getAutoGenerateSummaries()) {
+ summarize(e.getBibEntry(), bibDatabaseContext);
+ }
+ }
+ }
+
/**
* Start generating summary of a {@link BibEntry}, if it was already generated.
* This method returns a {@link ProcessingInfo} that can be used for tracking state of the summarization.
@@ -63,27 +99,52 @@ public SummariesService(AiPreferences aiPreferences,
* on the same {@link BibEntry}, the method will return the same {@link ProcessingInfo}.
*/
public ProcessingInfo summarize(BibEntry bibEntry, BibDatabaseContext bibDatabaseContext) {
- return summariesStatusMap.computeIfAbsent(bibEntry, file -> {
- ProcessingInfo processingInfo = new ProcessingInfo<>(bibEntry, ProcessingState.PROCESSING);
- generateSummary(bibEntry, bibDatabaseContext, processingInfo);
- return processingInfo;
- });
+ ProcessingInfo processingInfo = getProcessingInfo(bibEntry);
+
+ if (processingInfo.getState() == ProcessingState.STOPPED) {
+ startSummarizationTask(bibEntry, bibDatabaseContext, processingInfo);
+ }
+
+ return processingInfo;
}
- private void generateSummary(BibEntry bibEntry, BibDatabaseContext bibDatabaseContext, ProcessingInfo processingInfo) {
- if (bibDatabaseContext.getDatabasePath().isEmpty()) {
- runGenerateSummaryTask(processingInfo, bibEntry, bibDatabaseContext);
- } else if (bibEntry.getCitationKey().isEmpty() || CitationKeyCheck.citationKeyIsPresentAndUnique(bibDatabaseContext, bibEntry)) {
- runGenerateSummaryTask(processingInfo, bibEntry, bibDatabaseContext);
- } else {
- Optional summary = summariesStorage.get(bibDatabaseContext.getDatabasePath().get(), bibEntry.getCitationKey().get());
+ public ProcessingInfo getProcessingInfo(BibEntry entry) {
+ return summariesStatusMap.computeIfAbsent(entry, file -> new ProcessingInfo<>(entry, ProcessingState.STOPPED));
+ }
- if (summary.isEmpty()) {
- runGenerateSummaryTask(processingInfo, bibEntry, bibDatabaseContext);
- } else {
- processingInfo.setSuccess(summary.get());
- }
+ public List> getProcessingInfo(List entries) {
+ return entries.stream().map(this::getProcessingInfo).toList();
+ }
+
+ public List> summarize(StringProperty groupName, List entries, BibDatabaseContext bibDatabaseContext) {
+ List> result = getProcessingInfo(entries);
+
+ if (listsUnderSummarization.contains(entries)) {
+ return result;
}
+
+ listsUnderSummarization.add(entries);
+
+ List> needToProcess = result.stream().filter(processingInfo -> processingInfo.getState() == ProcessingState.STOPPED).toList();
+ startSummarizationTask(groupName, needToProcess, bibDatabaseContext);
+
+ return result;
+ }
+
+ private void startSummarizationTask(BibEntry entry, BibDatabaseContext bibDatabaseContext, ProcessingInfo processingInfo) {
+ processingInfo.setState(ProcessingState.PROCESSING);
+
+ new GenerateSummaryTask(entry, bibDatabaseContext, summariesStorage, chatLanguageModel, shutdownSignal, aiPreferences, filePreferences)
+ .onSuccess(processingInfo::setSuccess)
+ .onFailure(processingInfo::setException)
+ .executeWith(taskExecutor);
+ }
+
+ private void startSummarizationTask(StringProperty groupName, List> entries, BibDatabaseContext bibDatabaseContext) {
+ entries.forEach(processingInfo -> processingInfo.setState(ProcessingState.PROCESSING));
+
+ new GenerateSummaryForSeveralTask(groupName, entries, bibDatabaseContext, summariesStorage, chatLanguageModel, shutdownSignal, aiPreferences, filePreferences, taskExecutor)
+ .executeWith(taskExecutor);
}
/**
@@ -101,37 +162,6 @@ public void regenerateSummary(BibEntry bibEntry, BibDatabaseContext bibDatabaseC
summariesStorage.clear(bibDatabaseContext.getDatabasePath().get(), bibEntry.getCitationKey().get());
}
- generateSummary(bibEntry, bibDatabaseContext, processingInfo);
- }
-
- private void runGenerateSummaryTask(ProcessingInfo processingInfo, BibEntry bibEntry, BibDatabaseContext bibDatabaseContext) {
- new GenerateSummaryTask(
- bibDatabaseContext,
- bibEntry.getCitationKey().orElse(""),
- bibEntry.getFiles(),
- chatLanguageModel,
- shutdownSignal,
- aiPreferences,
- filePreferences)
- .onSuccess(summary -> {
- Summary Summary = new Summary(
- LocalDateTime.now(),
- aiPreferences.getAiProvider(),
- aiPreferences.getSelectedChatModel(),
- summary
- );
-
- processingInfo.setSuccess(Summary);
-
- if (bibDatabaseContext.getDatabasePath().isEmpty()) {
- LOGGER.info("No database path is present. Summary will not be stored in the next sessions");
- } else if (CitationKeyCheck.citationKeyIsPresentAndUnique(bibDatabaseContext, bibEntry)) {
- LOGGER.info("No valid citation key is present. Summary will not be stored in the next sessions");
- } else {
- summariesStorage.set(bibDatabaseContext.getDatabasePath().get(), bibEntry.getCitationKey().get(), Summary);
- }
- })
- .onFailure(processingInfo::setException)
- .executeWith(taskExecutor);
+ startSummarizationTask(bibEntry, bibDatabaseContext, processingInfo);
}
}
diff --git a/src/main/java/org/jabref/gui/auximport/AuxParserResultViewModel.java b/src/main/java/org/jabref/logic/auxparser/AuxParserStatisticsProvider.java
similarity index 90%
rename from src/main/java/org/jabref/gui/auximport/AuxParserResultViewModel.java
rename to src/main/java/org/jabref/logic/auxparser/AuxParserStatisticsProvider.java
index 511ef2d168a..8509eb4cc86 100644
--- a/src/main/java/org/jabref/gui/auximport/AuxParserResultViewModel.java
+++ b/src/main/java/org/jabref/logic/auxparser/AuxParserStatisticsProvider.java
@@ -1,15 +1,14 @@
-package org.jabref.gui.auximport;
+package org.jabref.logic.auxparser;
import java.util.stream.Collectors;
-import org.jabref.logic.auxparser.AuxParserResult;
import org.jabref.logic.l10n.Localization;
-public class AuxParserResultViewModel {
+public class AuxParserStatisticsProvider {
private AuxParserResult auxParserResult;
- public AuxParserResultViewModel(AuxParserResult auxParserResult) {
+ public AuxParserStatisticsProvider(AuxParserResult auxParserResult) {
this.auxParserResult = auxParserResult;
}
diff --git a/src/main/java/org/jabref/logic/citationkeypattern/BracketedPattern.java b/src/main/java/org/jabref/logic/citationkeypattern/BracketedPattern.java
index 0447f748cef..519fcdafd34 100644
--- a/src/main/java/org/jabref/logic/citationkeypattern/BracketedPattern.java
+++ b/src/main/java/org/jabref/logic/citationkeypattern/BracketedPattern.java
@@ -209,11 +209,10 @@ public static String expandBrackets(String pattern, Character keywordDelimiter,
*/
public static Function expandBracketContent(Character keywordDelimiter, BibEntry entry, BibDatabase database) {
return (String bracket) -> {
- String expandedPattern;
List fieldParts = parseFieldAndModifiers(bracket);
// check whether there is a modifier on the end such as
// ":lower":
- expandedPattern = getFieldValue(entry, fieldParts.getFirst(), keywordDelimiter, database);
+ String expandedPattern = getFieldValue(entry, fieldParts.getFirst(), keywordDelimiter, database);
if (fieldParts.size() > 1) {
// apply modifiers:
expandedPattern = applyModifiers(expandedPattern, fieldParts, 1, expandBracketContent(keywordDelimiter, entry, database));
@@ -233,6 +232,7 @@ public static Function expandBracketContent(Character keywordDel
public static String expandBrackets(String pattern, Function bracketContentHandler) {
Objects.requireNonNull(pattern);
StringBuilder expandedPattern = new StringBuilder();
+ pattern = pattern.replace("\\\"", "\u0A17");
StringTokenizer parsedPattern = new StringTokenizer(pattern, "\\[]\"", true);
while (parsedPattern.hasMoreTokens()) {
@@ -254,7 +254,7 @@ public static String expandBrackets(String pattern, Function bra
}
}
- return expandedPattern.toString();
+ return expandedPattern.toString().replace("\u0A17", "\\\"");
}
/**
@@ -1140,11 +1140,19 @@ protected static List parseFieldAndModifiers(String arg) {
} else if (currentChar == '\\') {
if (escaped) {
escaped = false;
- current.append(currentChar);
+ // Only : needs to be escaped
+ // " -> regex("...", "...") - escaping should be passed through to the regex parser
+ // : -> :abc:def
+ current.append('\\');
+ current.append('\\');
} else {
escaped = true;
}
} else if (escaped) {
+ if (currentChar != ':') {
+ // Only : needs to be escaped
+ current.append('\\');
+ }
current.append(currentChar);
escaped = false;
} else {
diff --git a/src/main/java/org/jabref/logic/exporter/ExporterFactory.java b/src/main/java/org/jabref/logic/exporter/ExporterFactory.java
index 8f5f61afdf3..28a89ebfe7c 100644
--- a/src/main/java/org/jabref/logic/exporter/ExporterFactory.java
+++ b/src/main/java/org/jabref/logic/exporter/ExporterFactory.java
@@ -13,7 +13,6 @@
import org.jabref.logic.util.StandardFileType;
import org.jabref.logic.xmp.XmpPreferences;
import org.jabref.model.database.BibDatabaseMode;
-import org.jabref.model.entry.BibEntryTypesManager;
import org.jabref.model.metadata.SelfContainedSaveOrder;
public class ExporterFactory {
@@ -24,9 +23,7 @@ private ExporterFactory(List exporters) {
this.exporters = Objects.requireNonNull(exporters);
}
- public static ExporterFactory create(CliPreferences preferences,
- BibEntryTypesManager entryTypesManager) {
-
+ public static ExporterFactory create(CliPreferences preferences) {
List customFormats = preferences.getExportPreferences().getCustomExporters();
LayoutFormatterPreferences layoutPreferences = preferences.getLayoutFormatterPreferences();
SelfContainedSaveOrder saveOrder = SelfContainedSaveOrder.of(preferences.getSelfContainedExportConfiguration().getSaveOrder());
@@ -61,7 +58,7 @@ public static ExporterFactory create(CliPreferences preferences,
exporters.add(new ModsExporter());
exporters.add(new XmpExporter(xmpPreferences));
exporters.add(new XmpPdfExporter(xmpPreferences));
- exporters.add(new EmbeddedBibFilePdfExporter(bibDatabaseMode, entryTypesManager, fieldPreferences));
+ exporters.add(new EmbeddedBibFilePdfExporter(bibDatabaseMode, preferences.getCustomEntryTypesRepository(), fieldPreferences));
exporters.add(new CffExporter());
exporters.add(new EndnoteXmlExporter(preferences.getBibEntryPreferences()));
diff --git a/src/main/java/org/jabref/logic/externalfiles/LinkedFileHandler.java b/src/main/java/org/jabref/logic/externalfiles/LinkedFileHandler.java
index b7ef25f335f..eebfe864e75 100644
--- a/src/main/java/org/jabref/logic/externalfiles/LinkedFileHandler.java
+++ b/src/main/java/org/jabref/logic/externalfiles/LinkedFileHandler.java
@@ -86,7 +86,16 @@ public boolean renameToName(String targetFileName, boolean overwriteExistingFile
}
final Path oldPath = oldFile.get();
- final Path newPath = oldPath.resolveSibling(targetFileName);
+ Optional oldExtension = FileUtil.getFileExtension(oldPath);
+ Optional newExtension = FileUtil.getFileExtension(targetFileName);
+
+ Path newPath;
+ if (newExtension.isPresent() || (oldExtension.isEmpty() && newExtension.isEmpty())) {
+ newPath = oldPath.resolveSibling(targetFileName);
+ } else {
+ assert oldExtension.isPresent() && newExtension.isEmpty();
+ newPath = oldPath.resolveSibling(targetFileName + "." + oldExtension.get());
+ }
String expandedOldFilePath = oldPath.toString();
boolean pathsDifferOnlyByCase = newPath.toString().equalsIgnoreCase(expandedOldFilePath)
diff --git a/src/main/java/org/jabref/logic/formatter/bibtexfields/RegexFormatter.java b/src/main/java/org/jabref/logic/formatter/bibtexfields/RegexFormatter.java
index 26859a82699..97bf67f0cff 100644
--- a/src/main/java/org/jabref/logic/formatter/bibtexfields/RegexFormatter.java
+++ b/src/main/java/org/jabref/logic/formatter/bibtexfields/RegexFormatter.java
@@ -15,26 +15,34 @@
public class RegexFormatter extends Formatter {
public static final String KEY = "regex";
+
private static final Logger LOGGER = LoggerFactory.getLogger(RegexFormatter.class);
+
private static final Pattern ESCAPED_OPENING_CURLY_BRACE = Pattern.compile("\\\\\\{");
private static final Pattern ESCAPED_CLOSING_CURLY_BRACE = Pattern.compile("\\\\\\}");
+
/**
* Matches text enclosed in curly brackets. The capturing group is used to prevent part of the input from being
* replaced.
*/
private static final Pattern ENCLOSED_IN_CURLY_BRACES = Pattern.compile("\\{.*?}");
+
private static final String REGEX_CAPTURING_GROUP = "regex";
private static final String REPLACEMENT_CAPTURING_GROUP = "replacement";
+
/**
* Matches a valid argument to the constructor. Two capturing groups are used to parse the {@link
* RegexFormatter#regex} and {@link RegexFormatter#replacement} used in {@link RegexFormatter#format(String)}
*/
private static final Pattern CONSTRUCTOR_ARGUMENT = Pattern.compile(
"^\\(\"(?<" + REGEX_CAPTURING_GROUP + ">.*?)\" *?, *?\"(?<" + REPLACEMENT_CAPTURING_GROUP + ">.*)\"\\)$");
+
// Magic arbitrary unicode char, which will never appear in bibtex files
private static final String PLACEHOLDER_FOR_PROTECTED_GROUP = Character.toString('\u0A14');
private static final String PLACEHOLDER_FOR_OPENING_CURLY_BRACE = Character.toString('\u0A15');
private static final String PLACEHOLDER_FOR_CLOSING_CURLY_BRACE = Character.toString('\u0A16');
+ private static final String PLACEHOLDER_FOR_QUOTE_SIGN = Character.toString('\u0A17');
+
private final String regex;
private final String replacement;
@@ -46,11 +54,11 @@ public class RegexFormatter extends Formatter {
*/
public RegexFormatter(String input) {
Objects.requireNonNull(input);
- input = input.trim();
+ input = input.trim().replace("\\\"", PLACEHOLDER_FOR_QUOTE_SIGN);
Matcher constructorArgument = CONSTRUCTOR_ARGUMENT.matcher(input);
if (constructorArgument.matches()) {
- regex = constructorArgument.group(REGEX_CAPTURING_GROUP);
- replacement = constructorArgument.group(REPLACEMENT_CAPTURING_GROUP);
+ regex = constructorArgument.group(REGEX_CAPTURING_GROUP).replace(PLACEHOLDER_FOR_QUOTE_SIGN, "\"");
+ replacement = constructorArgument.group(REPLACEMENT_CAPTURING_GROUP).replace(PLACEHOLDER_FOR_QUOTE_SIGN, "\"");
} else {
regex = null;
replacement = null;
diff --git a/src/main/java/org/jabref/logic/importer/ImportFormatPreferences.java b/src/main/java/org/jabref/logic/importer/ImportFormatPreferences.java
index 9d113c6b970..7ffa747719e 100644
--- a/src/main/java/org/jabref/logic/importer/ImportFormatPreferences.java
+++ b/src/main/java/org/jabref/logic/importer/ImportFormatPreferences.java
@@ -2,7 +2,7 @@
import org.jabref.logic.bibtex.FieldPreferences;
import org.jabref.logic.citationkeypattern.CitationKeyPatternPreferences;
-import org.jabref.logic.importer.fetcher.GrobidPreferences;
+import org.jabref.logic.importer.util.GrobidPreferences;
import org.jabref.logic.preferences.DOIPreferences;
import org.jabref.logic.xmp.XmpPreferences;
import org.jabref.model.entry.BibEntryPreferences;
diff --git a/src/main/java/org/jabref/logic/importer/ImportFormatReader.java b/src/main/java/org/jabref/logic/importer/ImportFormatReader.java
index cb963a46c57..068f6452071 100644
--- a/src/main/java/org/jabref/logic/importer/ImportFormatReader.java
+++ b/src/main/java/org/jabref/logic/importer/ImportFormatReader.java
@@ -28,7 +28,7 @@
import org.jabref.logic.importer.fileformat.PdfEmbeddedBibFileImporter;
import org.jabref.logic.importer.fileformat.PdfGrobidImporter;
import org.jabref.logic.importer.fileformat.PdfMergeMetadataImporter;
-import org.jabref.logic.importer.fileformat.PdfVerbatimBibTextImporter;
+import org.jabref.logic.importer.fileformat.PdfVerbatimBibtexImporter;
import org.jabref.logic.importer.fileformat.PdfXmpImporter;
import org.jabref.logic.importer.fileformat.RepecNepImporter;
import org.jabref.logic.importer.fileformat.RisImporter;
@@ -75,7 +75,7 @@ public void reset() {
formats.add(new MsBibImporter());
formats.add(new OvidImporter());
formats.add(new PdfMergeMetadataImporter(importFormatPreferences));
- formats.add(new PdfVerbatimBibTextImporter(importFormatPreferences));
+ formats.add(new PdfVerbatimBibtexImporter(importFormatPreferences));
formats.add(new PdfContentImporter());
formats.add(new PdfEmbeddedBibFileImporter(importFormatPreferences));
if (importFormatPreferences.grobidPreferences().isGrobidEnabled()) {
diff --git a/src/main/java/org/jabref/logic/importer/ImporterPreferences.java b/src/main/java/org/jabref/logic/importer/ImporterPreferences.java
index c706ff46799..64df8d2c9bc 100644
--- a/src/main/java/org/jabref/logic/importer/ImporterPreferences.java
+++ b/src/main/java/org/jabref/logic/importer/ImporterPreferences.java
@@ -2,6 +2,7 @@
import java.nio.file.Path;
import java.util.List;
+import java.util.Map;
import java.util.Optional;
import java.util.Set;
@@ -14,34 +15,42 @@
import javafx.collections.ObservableSet;
import org.jabref.logic.importer.fileformat.CustomImporter;
+import org.jabref.logic.importer.plaincitation.PlainCitationParserChoice;
import org.jabref.logic.preferences.FetcherApiKey;
public class ImporterPreferences {
-
private final BooleanProperty importerEnabled;
private final BooleanProperty generateNewKeyOnImport;
private final BooleanProperty warnAboutDuplicatesOnImport;
private final ObjectProperty importWorkingDirectory;
private final ObservableSet apiKeys;
+ private final Map defaultApiKeys;
private final ObservableSet customImporters;
private final BooleanProperty persistCustomKeys;
private final ObservableList catalogs;
+ private final ObjectProperty defaultPlainCitationParser;
+
public ImporterPreferences(boolean importerEnabled,
boolean generateNewKeyOnImport,
Path importWorkingDirectory,
boolean warnAboutDuplicatesOnImport,
Set customImporters,
Set apiKeys,
+ Map defaultApiKeys,
boolean persistCustomKeys,
- List catalogs) {
+ List catalogs,
+ PlainCitationParserChoice defaultPlainCitationParser
+ ) {
this.importerEnabled = new SimpleBooleanProperty(importerEnabled);
this.generateNewKeyOnImport = new SimpleBooleanProperty(generateNewKeyOnImport);
this.importWorkingDirectory = new SimpleObjectProperty<>(importWorkingDirectory);
this.warnAboutDuplicatesOnImport = new SimpleBooleanProperty(warnAboutDuplicatesOnImport);
this.customImporters = FXCollections.observableSet(customImporters);
this.apiKeys = FXCollections.observableSet(apiKeys);
+ this.defaultApiKeys = defaultApiKeys;
this.persistCustomKeys = new SimpleBooleanProperty(persistCustomKeys);
this.catalogs = FXCollections.observableArrayList(catalogs);
+ this.defaultPlainCitationParser = new SimpleObjectProperty<>(defaultPlainCitationParser);
}
public boolean areImporterEnabled() {
@@ -117,12 +126,17 @@ public void setPersistCustomKeys(boolean persistCustomKeys) {
this.persistCustomKeys.set(persistCustomKeys);
}
+ /**
+ * @param name of the fetcher
+ * @return either a customized API key if configured or the default key
+ */
public Optional getApiKey(String name) {
return apiKeys.stream()
.filter(key -> key.getName().equalsIgnoreCase(name))
.filter(FetcherApiKey::shouldUse)
.findFirst()
- .map(FetcherApiKey::getKey);
+ .map(FetcherApiKey::getKey)
+ .or(() -> Optional.ofNullable(defaultApiKeys.get(name)));
}
public void setCatalogs(List catalogs) {
@@ -133,4 +147,16 @@ public void setCatalogs(List catalogs) {
public ObservableList getCatalogs() {
return catalogs;
}
+
+ public PlainCitationParserChoice getDefaultPlainCitationParser() {
+ return defaultPlainCitationParser.get();
+ }
+
+ public ObjectProperty defaultPlainCitationParserProperty() {
+ return defaultPlainCitationParser;
+ }
+
+ public void setDefaultPlainCitationParser(PlainCitationParserChoice defaultPlainCitationParser) {
+ this.defaultPlainCitationParser.set(defaultPlainCitationParser);
+ }
}
diff --git a/src/main/java/org/jabref/logic/importer/fetcher/AstrophysicsDataSystem.java b/src/main/java/org/jabref/logic/importer/fetcher/AstrophysicsDataSystem.java
index 373cd064427..df90f2b076e 100644
--- a/src/main/java/org/jabref/logic/importer/fetcher/AstrophysicsDataSystem.java
+++ b/src/main/java/org/jabref/logic/importer/fetcher/AstrophysicsDataSystem.java
@@ -30,7 +30,6 @@
import org.jabref.logic.importer.fetcher.transformers.DefaultQueryTransformer;
import org.jabref.logic.importer.fileformat.BibtexParser;
import org.jabref.logic.net.URLDownload;
-import org.jabref.logic.util.BuildInfo;
import org.jabref.model.entry.BibEntry;
import org.jabref.model.entry.field.StandardField;
import org.jabref.model.entry.field.UnknownField;
@@ -48,11 +47,11 @@
*/
public class AstrophysicsDataSystem
implements IdBasedParserFetcher, PagedSearchBasedParserFetcher, EntryBasedParserFetcher, CustomizableKeyFetcher {
+ public static final String FETCHER_NAME = "SAO/NASA ADS";
private static final String API_SEARCH_URL = "https://api.adsabs.harvard.edu/v1/search/query";
private static final String API_EXPORT_URL = "https://api.adsabs.harvard.edu/v1/export/bibtexabs";
- private static final String API_KEY = new BuildInfo().astrophysicsDataSystemAPIKey;
private final ImportFormatPreferences preferences;
private final ImporterPreferences importerPreferences;
@@ -79,7 +78,7 @@ private static URL getURLforExport() throws URISyntaxException, MalformedURLExce
@Override
public String getName() {
- return "SAO/NASA ADS";
+ return FETCHER_NAME;
}
/**
@@ -255,7 +254,7 @@ private List performSearchByIds(Collection identifiers) throws
try {
String postData = buildPostData(ids);
URLDownload download = new URLDownload(urLforExport);
- download.addHeader("Authorization", "Bearer " + importerPreferences.getApiKey(getName()).orElse(API_KEY));
+ importerPreferences.getApiKey(getName()).ifPresent(key -> download.addHeader("Authorization", "Bearer " + key));
download.addHeader("ContentType", "application/json");
download.setPostData(postData);
String content = download.asString();
@@ -308,7 +307,7 @@ public Page performSearchPaged(QueryNode luceneQuery, int pageNumber)
@Override
public URLDownload getUrlDownload(URL url) {
URLDownload urlDownload = new URLDownload(url);
- urlDownload.addHeader("Authorization", "Bearer " + importerPreferences.getApiKey(getName()).orElse(API_KEY));
+ importerPreferences.getApiKey(getName()).ifPresent(key -> urlDownload.addHeader("Authorization", "Bearer " + key));
return urlDownload;
}
}
diff --git a/src/main/java/org/jabref/logic/importer/fetcher/BiodiversityLibrary.java b/src/main/java/org/jabref/logic/importer/fetcher/BiodiversityLibrary.java
index 99ce61c4ca9..f5154d60b23 100644
--- a/src/main/java/org/jabref/logic/importer/fetcher/BiodiversityLibrary.java
+++ b/src/main/java/org/jabref/logic/importer/fetcher/BiodiversityLibrary.java
@@ -16,7 +16,6 @@
import org.jabref.logic.importer.fetcher.transformers.BiodiversityLibraryTransformer;
import org.jabref.logic.importer.util.JsonReader;
import org.jabref.logic.net.URLDownload;
-import org.jabref.logic.util.BuildInfo;
import org.jabref.model.entry.Author;
import org.jabref.model.entry.AuthorList;
import org.jabref.model.entry.BibEntry;
@@ -37,14 +36,12 @@
* @see API documentation
*/
public class BiodiversityLibrary implements SearchBasedParserFetcher, CustomizableKeyFetcher {
+ public static final String FETCHER_NAME = "Biodiversity Heritage";
- private static final String API_KEY = new BuildInfo().biodiversityHeritageApiKey;
private static final String BASE_URL = "https://www.biodiversitylibrary.org/api3";
private static final String RESPONSE_FORMAT = "json";
private static final String TEST_URL_WITHOUT_API_KEY = "https://www.biodiversitylibrary.org/api3?apikey=";
- private static final String FETCHER_NAME = "Biodiversity Heritage";
-
private final ImporterPreferences importerPreferences;
public BiodiversityLibrary(ImporterPreferences importerPreferences) {
@@ -63,7 +60,7 @@ public String getTestUrl() {
public URL getBaseURL() throws URISyntaxException, MalformedURLException {
URIBuilder baseURI = new URIBuilder(BASE_URL);
- baseURI.addParameter("apikey", importerPreferences.getApiKey(getName()).orElse(API_KEY));
+ importerPreferences.getApiKey(getName()).ifPresent(key -> baseURI.addParameter("apikey", key));
baseURI.addParameter("format", RESPONSE_FORMAT);
return baseURI.build().toURL();
diff --git a/src/main/java/org/jabref/logic/importer/fetcher/GrobidCitationFetcher.java b/src/main/java/org/jabref/logic/importer/fetcher/GrobidCitationFetcher.java
deleted file mode 100644
index 47d4fc35c14..00000000000
--- a/src/main/java/org/jabref/logic/importer/fetcher/GrobidCitationFetcher.java
+++ /dev/null
@@ -1,93 +0,0 @@
-package org.jabref.logic.importer.fetcher;
-
-import java.io.IOException;
-import java.net.SocketTimeoutException;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
-import java.util.Optional;
-import java.util.stream.Collectors;
-
-import org.jabref.http.dto.SimpleHttpResponse;
-import org.jabref.logic.importer.FetcherException;
-import org.jabref.logic.importer.ImportFormatPreferences;
-import org.jabref.logic.importer.ParseException;
-import org.jabref.logic.importer.SearchBasedFetcher;
-import org.jabref.logic.importer.util.GrobidService;
-import org.jabref.model.entry.BibEntry;
-
-import org.apache.lucene.queryparser.flexible.core.nodes.QueryNode;
-import org.jooq.lambda.Unchecked;
-import org.jooq.lambda.UncheckedException;
-import org.jsoup.HttpStatusException;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-public class GrobidCitationFetcher implements SearchBasedFetcher {
-
- private static final Logger LOGGER = LoggerFactory.getLogger(GrobidCitationFetcher.class);
-
- private final ImportFormatPreferences importFormatPreferences;
- private final GrobidService grobidService;
-
- public GrobidCitationFetcher(GrobidPreferences grobidPreferences, ImportFormatPreferences importFormatPreferences) {
- this(importFormatPreferences, new GrobidService(grobidPreferences));
- }
-
- GrobidCitationFetcher(ImportFormatPreferences importFormatPreferences, GrobidService grobidService) {
- this.importFormatPreferences = importFormatPreferences;
- this.grobidService = grobidService;
- }
-
- /**
- * Passes request to grobid server, using consolidateCitations option to improve result. Takes a while, since the
- * server has to look up the entry.
- *
- * @return A BibTeX string if extraction is successful
- */
- private Optional parseUsingGrobid(String plainText) throws FetcherException {
- try {
- return grobidService.processCitation(plainText, importFormatPreferences, GrobidService.ConsolidateCitations.WITH_METADATA);
- } catch (HttpStatusException e) {
- LOGGER.debug("Could not connect to Grobid", e);
- throw new FetcherException("{grobid}", new SimpleHttpResponse(e));
- } catch (SocketTimeoutException e) {
- String msg = "Connection timed out.";
- LOGGER.debug(msg, e);
- throw new FetcherException(msg, e.getCause());
- } catch (IOException | ParseException e) {
- LOGGER.debug("Could not process citation", e);
- throw new FetcherException("Could not process citation", e);
- }
- }
-
- @Override
- public String getName() {
- return "GROBID";
- }
-
- @Override
- public List performSearch(String searchQuery) throws FetcherException {
- List