-
-
Notifications
You must be signed in to change notification settings - Fork 3.1k
Feat: Implement a basic Zotero picker compatible CAYW endpoint #13185
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
ea620a5
7d57d0e
0877a93
546b32a
9b52f2f
7c2826f
4911107
795d229
fa06686
d96ed1e
18e105d
91ffba8
2e49274
d2da505
b72a1ce
015d5a3
2f895ec
0626829
0a6743b
0825582
7e9c94f
85ef99e
04a1fab
62c7929
c3828cf
edf2b71
f516650
54f3937
90e4c96
6bc30a0
22200fc
59cece2
99ff753
89d09f8
f7de70a
a9d66b2
caca932
f32016f
56eabd3
64ae673
15da4da
cd9fb2f
54eef3d
06a2d5a
70865c1
92dadba
0b17fa0
4f516fc
889bc1b
5822b8b
5389fdc
ceebe6d
e3176ee
bd88c8a
f018a9a
dd2ac4f
d58a196
4986c3f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,191 @@ | ||
| package org.jabref.http.server.cayw; | ||
|
|
||
| import java.io.BufferedReader; | ||
| import java.io.IOException; | ||
| import java.io.InputStream; | ||
| import java.io.InputStreamReader; | ||
| import java.nio.charset.StandardCharsets; | ||
| import java.nio.file.Files; | ||
| import java.util.ArrayList; | ||
| import java.util.List; | ||
| import java.util.Optional; | ||
| import java.util.concurrent.CompletableFuture; | ||
| import java.util.concurrent.CountDownLatch; | ||
| import java.util.concurrent.ExecutionException; | ||
|
|
||
| import javafx.application.Platform; | ||
|
|
||
| import org.jabref.http.server.cayw.gui.CAYWEntry; | ||
| import org.jabref.http.server.cayw.gui.SearchDialog; | ||
| import org.jabref.logic.importer.fileformat.BibtexImporter; | ||
| import org.jabref.logic.preferences.CliPreferences; | ||
| import org.jabref.logic.preferences.JabRefCliPreferences; | ||
| import org.jabref.model.database.BibDatabase; | ||
| import org.jabref.model.database.BibDatabaseContext; | ||
| import org.jabref.model.entry.BibEntry; | ||
| import org.jabref.model.entry.field.StandardField; | ||
| import org.jabref.model.util.DummyFileUpdateMonitor; | ||
|
|
||
| import com.google.gson.Gson; | ||
| import jakarta.inject.Inject; | ||
| import jakarta.ws.rs.DefaultValue; | ||
| import jakarta.ws.rs.GET; | ||
| import jakarta.ws.rs.Path; | ||
| import jakarta.ws.rs.Produces; | ||
| import jakarta.ws.rs.QueryParam; | ||
| import jakarta.ws.rs.core.MediaType; | ||
| import jakarta.ws.rs.core.Response; | ||
| import org.jspecify.annotations.Nullable; | ||
| import org.slf4j.Logger; | ||
| import org.slf4j.LoggerFactory; | ||
|
|
||
| @Path("better-bibtex/cayw") | ||
| public class CAYWResource { | ||
| public static final Logger LOGGER = LoggerFactory.getLogger(CAYWResource.class); | ||
| private static final String CHOCOLATEBIB_PATH = "/Chocolate.bib"; | ||
| private static boolean initialized = false; | ||
palukku marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| @Inject | ||
| private CliPreferences preferences; | ||
|
|
||
| @Inject | ||
| private Gson gson; | ||
|
|
||
| @GET | ||
| @Produces(MediaType.TEXT_PLAIN) | ||
| public Response getCitation( | ||
| @QueryParam("probe") String probe, | ||
palukku marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| @QueryParam("format") @DefaultValue("latex") String format, | ||
| @QueryParam("clipboard") String clipboard, | ||
| @QueryParam("minimize") String minimize, | ||
| @QueryParam("texstudio") String texstudio, | ||
| @QueryParam("selected") String selected, | ||
| @QueryParam("select") String select, | ||
| @QueryParam("librarypath") String libraryPath | ||
| ) throws IOException, ExecutionException, InterruptedException { | ||
| if (probe != null && !probe.isEmpty()) { | ||
| return Response.ok("ready").build(); | ||
| } | ||
|
|
||
| BibDatabaseContext databaseContext = getBibDatabaseContext(libraryPath); | ||
|
|
||
| /* unused until DatabaseSearcher is fixed | ||
| PostgreServer postgreServer = new PostgreServer(); | ||
| IndexManager.clearOldSearchIndices(); | ||
| searcher = new DatabaseSearcher( | ||
| databaseContext, | ||
| new CurrentThreadTaskExecutor(), | ||
| preferences, | ||
| postgreServer); | ||
| */ | ||
|
|
||
| List<CAYWEntry<BibEntry>> entries = databaseContext.getEntries() | ||
| .stream() | ||
| .map(this::createCAYWEntry) | ||
| .toList(); | ||
|
|
||
| initializeGUI(); | ||
|
|
||
| CompletableFuture<List<BibEntry>> future = new CompletableFuture<>(); | ||
| Platform.runLater(() -> { | ||
| SearchDialog<BibEntry> dialog = new SearchDialog<>(); | ||
|
Comment on lines
+90
to
+91
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. GUI code is directly implemented in the resource class instead of being delegated to a proper logic layer in org.jabref.logic package. |
||
| // TODO: Using the DatabaseSearcher directly here results in a lot of exceptions being thrown, so we use an alternative for now until we have a nice way of using the DatabaseSearcher class. | ||
| // searchDialog.set(new SearchDialog<>(s -> searcher.getMatches(new SearchQuery(s)), entries)); | ||
| List<BibEntry> results = dialog.show(searchQuery -> | ||
| entries.stream() | ||
| .filter(bibEntryCAYWEntry -> matches(bibEntryCAYWEntry, searchQuery)) | ||
| .map(CAYWEntry::getValue) | ||
| .toList(), | ||
| entries); | ||
|
|
||
| future.complete(results); | ||
| }); | ||
|
|
||
| List<String> citationKeys = future.get().stream() | ||
| .map(BibEntry::getCitationKey) | ||
| .filter(Optional::isPresent) | ||
| .map(Optional::get) | ||
| .toList(); | ||
|
|
||
| if (citationKeys.isEmpty()) { | ||
| return Response.noContent().build(); | ||
| } | ||
|
|
||
| return Response.ok(gson.toJson(citationKeys)).build(); | ||
| } | ||
|
|
||
| private BibDatabaseContext getBibDatabaseContext(String libraryPath) throws IOException { | ||
| InputStream libraryStream; | ||
| if (libraryPath != null && !libraryPath.isEmpty()) { | ||
| java.nio.file.Path path = java.nio.file.Path.of(libraryPath); | ||
| if (!Files.exists(path)) { | ||
| LOGGER.error("Library path does not exist, using the default chocolate.bib: {}", libraryPath); | ||
| libraryStream = getChocolateBibAsStream(); | ||
| } else { | ||
| libraryStream = Files.newInputStream(path); | ||
| } | ||
| } else { | ||
| // Use the latest opened library as the default library | ||
| final List<java.nio.file.Path> lastOpenedLibraries = new ArrayList<>(JabRefCliPreferences.getInstance().getLastFilesOpenedPreferences().getLastFilesOpened()); | ||
| if (lastOpenedLibraries.isEmpty()) { | ||
| LOGGER.warn("No library path provided and no last opened libraries found, using the default chocolate.bib."); | ||
| libraryStream = getChocolateBibAsStream(); | ||
| } else { | ||
| java.nio.file.Path lastOpenedLibrary = lastOpenedLibraries.getFirst(); | ||
| if (!Files.exists(lastOpenedLibrary)) { | ||
| LOGGER.error("Last opened library does not exist, using the default chocolate.bib: {}", lastOpenedLibrary); | ||
| libraryStream = getChocolateBibAsStream(); | ||
| } else { | ||
| libraryStream = Files.newInputStream(lastOpenedLibrary); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| BibtexImporter bibtexImporter = new BibtexImporter(preferences.getImportFormatPreferences(), new DummyFileUpdateMonitor()); | ||
| BibDatabaseContext databaseContext; | ||
| try (BufferedReader reader = new BufferedReader(new InputStreamReader(libraryStream, StandardCharsets.UTF_8))) { | ||
| databaseContext = bibtexImporter.importDatabase(reader).getDatabaseContext(); | ||
| } | ||
| return databaseContext; | ||
| } | ||
|
|
||
| private synchronized void initializeGUI() { | ||
| // TODO: Implement a better way to handle the window popup since this is a bit hacky. | ||
| if (!initialized) { | ||
| CountDownLatch latch = new CountDownLatch(1); | ||
| Platform.startup(() -> { | ||
| Platform.setImplicitExit(false); | ||
| initialized = true; | ||
| latch.countDown(); | ||
| }); | ||
| try { | ||
| latch.await(); | ||
| } catch (InterruptedException e) { | ||
| Thread.currentThread().interrupt(); | ||
| throw new RuntimeException("JavaFX initialization interrupted", e); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /// @return a stream to the `Chocolate.bib` file in the classpath (is null only if the file was moved or there are issues with the classpath) | ||
| private @Nullable InputStream getChocolateBibAsStream() { | ||
palukku marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| return BibDatabase.class.getResourceAsStream(CHOCOLATEBIB_PATH); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Method can return null but doesn't use Optional to explicitly handle the null case, violating the principle of not returning null from public methods. |
||
| } | ||
|
|
||
| private CAYWEntry<BibEntry> createCAYWEntry(BibEntry entry) { | ||
| String label = entry.getCitationKey().orElse(""); | ||
| String shortLabel = label; | ||
| String description = entry.getField(StandardField.TITLE).orElse(entry.getAuthorTitleYear()); | ||
| return new CAYWEntry<>(entry, label, shortLabel, description); | ||
| } | ||
|
|
||
| private boolean matches(CAYWEntry<BibEntry> entry, String searchText) { | ||
| if (searchText == null || searchText.isEmpty()) { | ||
| return true; | ||
| } | ||
| String lowerSearchText = searchText.toLowerCase(); | ||
| return entry.getLabel().toLowerCase().contains(lowerSearchText) || | ||
| entry.getDescription().toLowerCase().contains(lowerSearchText) || | ||
| entry.getShortLabel().toLowerCase().contains(lowerSearchText); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| package org.jabref.http.server.cayw.gui; | ||
|
|
||
| import javafx.event.ActionEvent; | ||
| import javafx.event.EventHandler; | ||
|
|
||
| public class CAYWEntry<T> { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. BTW, what about |
||
|
|
||
| private final T value; | ||
|
|
||
| // Used on the buttons ("chips") | ||
| private final String shortLabel; | ||
|
|
||
| // Used in the list | ||
| private final String label; | ||
|
|
||
| // Used when hovering and used as bases on the second line | ||
| private final String description; | ||
|
|
||
| private EventHandler<ActionEvent> onClick; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The onClick field can be null since it's not initialized in constructor and has no default value. This violates modern Java practices of avoiding null values in public methods.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can't believe trag-bot gives good and/or interesting comments |
||
|
|
||
| public CAYWEntry(T value, String label, String shortLabel, String description) { | ||
| this.value = value; | ||
| this.label = label; | ||
| this.shortLabel = shortLabel; | ||
| this.description = description; | ||
| } | ||
|
|
||
| public T getValue() { | ||
| return value; | ||
| } | ||
|
|
||
| public String getLabel() { | ||
| return label; | ||
| } | ||
|
|
||
| public String getShortLabel() { | ||
| return shortLabel; | ||
| } | ||
|
|
||
| public String getDescription() { | ||
| return description; | ||
| } | ||
|
|
||
| public EventHandler<ActionEvent> getOnClick() { | ||
| return onClick; | ||
| } | ||
|
Comment on lines
+44
to
+46
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Method returns null when onClick is not set. Should return Optional<EventHandler> instead to make null-safety explicit in the API. |
||
|
|
||
| public void setOnClick(EventHandler<ActionEvent> onClick) { | ||
| this.onClick = onClick; | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.