Skip to content

Commit 7959b16

Browse files
palukkuInAnYansubhramitkoppor
authored
Implement a Zotero Picker compatible CAYW Endpoint (#13185)
Co-authored-by: Ruslan <ruslanpopov1512@gmail.com> Co-authored-by: Subhramit Basu <subhramit.bb@live.in> Co-authored-by: Oliver Kopp <kopp.dev@gmail.com>
1 parent 9e5e5df commit 7959b16

File tree

14 files changed

+652
-1
lines changed

14 files changed

+652
-1
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv
1515
- We distribute arm64 images for Linux. [#10842](https://github.com/JabRef/jabref/issues/10842)
1616
- We added the field `monthfiled` to the default list of fields to resolve BibTeX-Strings for [#13375](https://github.com/JabRef/jabref/issues/13375)
1717
- We added a new ID based fetcher for [EuropePMC](https://europepmc.org/). [#13389](https://github.com/JabRef/jabref/pull/13389)
18+
- We added an initial [cite as you write](https://retorque.re/zotero-better-bibtex/citing/cayw/) endpoint. [#13187](https://github.com/JabRef/jabref/issues/13187)
1819

1920
### Changed
2021

jablib/src/main/resources/l10n/JabRef_en.properties

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,8 @@ Application=Application
9191

9292
Application\ to\ push\ entries\ to=Application to push entries to
9393

94+
%0\ |\ Cite\ As\ You\ Write=%0 | Cite As You Write
95+
9496
Apply=Apply
9597

9698
Assign\ the\ original\ group's\ entries\ to\ this\ group?=Assign the original group's entries to this group?

jabsrv-cli/src/main/java/org/jabref/http/server/cli/ServerCli.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ public class ServerCli implements Callable<Void> {
2929
private String host = "localhost";
3030

3131
@CommandLine.Option(names = {"-p", "--port"}, description = "the port")
32-
private Integer port = 6050;
32+
private Integer port = 23119;
3333

3434
/**
3535
* Starts an http server serving the last files opened in JabRef<br>

jabsrv/src/main/java/module-info.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
exports org.jabref.http.dto to com.google.gson, org.glassfish.hk2.locator;
55

66
opens org.jabref.http.server to org.glassfish.hk2.utilities, org.glassfish.hk2.locator;
7+
exports org.jabref.http.server.cayw;
8+
opens org.jabref.http.server.cayw to org.glassfish.hk2.locator, org.glassfish.hk2.utilities;
79

810
// For ServiceLocatorUtilities.createAndPopulateServiceLocator()
911
requires org.glassfish.hk2.locator;

jabsrv/src/main/java/org/jabref/http/server/Server.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
import org.jabref.http.dto.GlobalExceptionMapper;
1111
import org.jabref.http.dto.GsonFactory;
12+
import org.jabref.http.server.cayw.CAYWResource;
1213
import org.jabref.http.server.services.FilesToServe;
1314
import org.jabref.logic.os.OS;
1415

@@ -55,6 +56,7 @@ private HttpServer startServer(ServiceLocator serviceLocator, URI uri) {
5556
resourceConfig.register(RootResource.class);
5657
resourceConfig.register(LibrariesResource.class);
5758
resourceConfig.register(LibraryResource.class);
59+
resourceConfig.register(CAYWResource.class);
5860
resourceConfig.register(CORSFilter.class);
5961
resourceConfig.register(GlobalExceptionMapper.class);
6062

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
package org.jabref.http.server.cayw;
2+
3+
import java.io.BufferedReader;
4+
import java.io.IOException;
5+
import java.io.InputStream;
6+
import java.io.InputStreamReader;
7+
import java.nio.charset.StandardCharsets;
8+
import java.nio.file.Files;
9+
import java.util.ArrayList;
10+
import java.util.List;
11+
import java.util.Optional;
12+
import java.util.concurrent.CompletableFuture;
13+
import java.util.concurrent.CountDownLatch;
14+
import java.util.concurrent.ExecutionException;
15+
16+
import javafx.application.Platform;
17+
18+
import org.jabref.http.server.cayw.gui.CAYWEntry;
19+
import org.jabref.http.server.cayw.gui.SearchDialog;
20+
import org.jabref.logic.importer.fileformat.BibtexImporter;
21+
import org.jabref.logic.preferences.CliPreferences;
22+
import org.jabref.logic.preferences.JabRefCliPreferences;
23+
import org.jabref.model.database.BibDatabase;
24+
import org.jabref.model.database.BibDatabaseContext;
25+
import org.jabref.model.entry.BibEntry;
26+
import org.jabref.model.entry.field.StandardField;
27+
import org.jabref.model.util.DummyFileUpdateMonitor;
28+
29+
import com.google.gson.Gson;
30+
import jakarta.inject.Inject;
31+
import jakarta.ws.rs.DefaultValue;
32+
import jakarta.ws.rs.GET;
33+
import jakarta.ws.rs.Path;
34+
import jakarta.ws.rs.Produces;
35+
import jakarta.ws.rs.QueryParam;
36+
import jakarta.ws.rs.core.MediaType;
37+
import jakarta.ws.rs.core.Response;
38+
import org.jspecify.annotations.Nullable;
39+
import org.slf4j.Logger;
40+
import org.slf4j.LoggerFactory;
41+
42+
@Path("better-bibtex/cayw")
43+
public class CAYWResource {
44+
public static final Logger LOGGER = LoggerFactory.getLogger(CAYWResource.class);
45+
private static final String CHOCOLATEBIB_PATH = "/Chocolate.bib";
46+
private static boolean initialized = false;
47+
48+
@Inject
49+
private CliPreferences preferences;
50+
51+
@Inject
52+
private Gson gson;
53+
54+
@GET
55+
@Produces(MediaType.TEXT_PLAIN)
56+
public Response getCitation(
57+
@QueryParam("probe") String probe,
58+
@QueryParam("format") @DefaultValue("latex") String format,
59+
@QueryParam("clipboard") String clipboard,
60+
@QueryParam("minimize") String minimize,
61+
@QueryParam("texstudio") String texstudio,
62+
@QueryParam("selected") String selected,
63+
@QueryParam("select") String select,
64+
@QueryParam("librarypath") String libraryPath
65+
) throws IOException, ExecutionException, InterruptedException {
66+
if (probe != null && !probe.isEmpty()) {
67+
return Response.ok("ready").build();
68+
}
69+
70+
BibDatabaseContext databaseContext = getBibDatabaseContext(libraryPath);
71+
72+
/* unused until DatabaseSearcher is fixed
73+
PostgreServer postgreServer = new PostgreServer();
74+
IndexManager.clearOldSearchIndices();
75+
searcher = new DatabaseSearcher(
76+
databaseContext,
77+
new CurrentThreadTaskExecutor(),
78+
preferences,
79+
postgreServer);
80+
*/
81+
82+
List<CAYWEntry<BibEntry>> entries = databaseContext.getEntries()
83+
.stream()
84+
.map(this::createCAYWEntry)
85+
.toList();
86+
87+
initializeGUI();
88+
89+
CompletableFuture<List<BibEntry>> future = new CompletableFuture<>();
90+
Platform.runLater(() -> {
91+
SearchDialog<BibEntry> dialog = new SearchDialog<>();
92+
// 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.
93+
// searchDialog.set(new SearchDialog<>(s -> searcher.getMatches(new SearchQuery(s)), entries));
94+
List<BibEntry> results = dialog.show(searchQuery ->
95+
entries.stream()
96+
.filter(bibEntryCAYWEntry -> matches(bibEntryCAYWEntry, searchQuery))
97+
.map(CAYWEntry::getValue)
98+
.toList(),
99+
entries);
100+
101+
future.complete(results);
102+
});
103+
104+
List<String> citationKeys = future.get().stream()
105+
.map(BibEntry::getCitationKey)
106+
.filter(Optional::isPresent)
107+
.map(Optional::get)
108+
.toList();
109+
110+
if (citationKeys.isEmpty()) {
111+
return Response.noContent().build();
112+
}
113+
114+
return Response.ok(gson.toJson(citationKeys)).build();
115+
}
116+
117+
private BibDatabaseContext getBibDatabaseContext(String libraryPath) throws IOException {
118+
InputStream libraryStream;
119+
if (libraryPath != null && !libraryPath.isEmpty()) {
120+
java.nio.file.Path path = java.nio.file.Path.of(libraryPath);
121+
if (!Files.exists(path)) {
122+
LOGGER.error("Library path does not exist, using the default chocolate.bib: {}", libraryPath);
123+
libraryStream = getChocolateBibAsStream();
124+
} else {
125+
libraryStream = Files.newInputStream(path);
126+
}
127+
} else {
128+
// Use the latest opened library as the default library
129+
final List<java.nio.file.Path> lastOpenedLibraries = new ArrayList<>(JabRefCliPreferences.getInstance().getLastFilesOpenedPreferences().getLastFilesOpened());
130+
if (lastOpenedLibraries.isEmpty()) {
131+
LOGGER.warn("No library path provided and no last opened libraries found, using the default chocolate.bib.");
132+
libraryStream = getChocolateBibAsStream();
133+
} else {
134+
java.nio.file.Path lastOpenedLibrary = lastOpenedLibraries.getFirst();
135+
if (!Files.exists(lastOpenedLibrary)) {
136+
LOGGER.error("Last opened library does not exist, using the default chocolate.bib: {}", lastOpenedLibrary);
137+
libraryStream = getChocolateBibAsStream();
138+
} else {
139+
libraryStream = Files.newInputStream(lastOpenedLibrary);
140+
}
141+
}
142+
}
143+
144+
BibtexImporter bibtexImporter = new BibtexImporter(preferences.getImportFormatPreferences(), new DummyFileUpdateMonitor());
145+
BibDatabaseContext databaseContext;
146+
try (BufferedReader reader = new BufferedReader(new InputStreamReader(libraryStream, StandardCharsets.UTF_8))) {
147+
databaseContext = bibtexImporter.importDatabase(reader).getDatabaseContext();
148+
}
149+
return databaseContext;
150+
}
151+
152+
private synchronized void initializeGUI() {
153+
// TODO: Implement a better way to handle the window popup since this is a bit hacky.
154+
if (!initialized) {
155+
CountDownLatch latch = new CountDownLatch(1);
156+
Platform.startup(() -> {
157+
Platform.setImplicitExit(false);
158+
initialized = true;
159+
latch.countDown();
160+
});
161+
try {
162+
latch.await();
163+
} catch (InterruptedException e) {
164+
Thread.currentThread().interrupt();
165+
throw new RuntimeException("JavaFX initialization interrupted", e);
166+
}
167+
}
168+
}
169+
170+
/// @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)
171+
private @Nullable InputStream getChocolateBibAsStream() {
172+
return BibDatabase.class.getResourceAsStream(CHOCOLATEBIB_PATH);
173+
}
174+
175+
private CAYWEntry<BibEntry> createCAYWEntry(BibEntry entry) {
176+
String label = entry.getCitationKey().orElse("");
177+
String shortLabel = label;
178+
String description = entry.getField(StandardField.TITLE).orElse(entry.getAuthorTitleYear());
179+
return new CAYWEntry<>(entry, label, shortLabel, description);
180+
}
181+
182+
private boolean matches(CAYWEntry<BibEntry> entry, String searchText) {
183+
if (searchText == null || searchText.isEmpty()) {
184+
return true;
185+
}
186+
String lowerSearchText = searchText.toLowerCase();
187+
return entry.getLabel().toLowerCase().contains(lowerSearchText) ||
188+
entry.getDescription().toLowerCase().contains(lowerSearchText) ||
189+
entry.getShortLabel().toLowerCase().contains(lowerSearchText);
190+
}
191+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package org.jabref.http.server.cayw.gui;
2+
3+
import javafx.event.ActionEvent;
4+
import javafx.event.EventHandler;
5+
6+
public class CAYWEntry<T> {
7+
8+
private final T value;
9+
10+
// Used on the buttons ("chips")
11+
private final String shortLabel;
12+
13+
// Used in the list
14+
private final String label;
15+
16+
// Used when hovering and used as bases on the second line
17+
private final String description;
18+
19+
private EventHandler<ActionEvent> onClick;
20+
21+
public CAYWEntry(T value, String label, String shortLabel, String description) {
22+
this.value = value;
23+
this.label = label;
24+
this.shortLabel = shortLabel;
25+
this.description = description;
26+
}
27+
28+
public T getValue() {
29+
return value;
30+
}
31+
32+
public String getLabel() {
33+
return label;
34+
}
35+
36+
public String getShortLabel() {
37+
return shortLabel;
38+
}
39+
40+
public String getDescription() {
41+
return description;
42+
}
43+
44+
public EventHandler<ActionEvent> getOnClick() {
45+
return onClick;
46+
}
47+
48+
public void setOnClick(EventHandler<ActionEvent> onClick) {
49+
this.onClick = onClick;
50+
}
51+
}

0 commit comments

Comments
 (0)