Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion jabsrv/src/main/java/module-info.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@
exports org.jabref.http.server;

exports org.jabref.http.dto to com.google.gson, org.glassfish.hk2.locator;
exports org.jabref.http.dto.cayw to com.google.gson;

opens org.jabref.http.server to org.glassfish.hk2.utilities, org.glassfish.hk2.locator;
exports org.jabref.http.server.cayw;
opens org.jabref.http.server.cayw to org.glassfish.hk2.locator, org.glassfish.hk2.utilities;
opens org.jabref.http.server.cayw to com.google.gson, org.glassfish.hk2.locator, org.glassfish.hk2.utilities;

// For ServiceLocatorUtilities.createAndPopulateServiceLocator()
requires org.glassfish.hk2.locator;
Expand Down
27 changes: 27 additions & 0 deletions jabsrv/src/main/java/org/jabref/http/dto/cayw/SimpleJson.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package org.jabref.http.dto.cayw;

import java.nio.charset.StandardCharsets;

import org.jabref.model.entry.BibEntry;

import com.google.common.hash.Hashing;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public record SimpleJson(
long id,
String citationKey) {

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

public static SimpleJson fromBibEntry(BibEntry bibEntry) {
if (bibEntry.getCitationKey().isEmpty()) {
LOGGER.warn("BibEntry has no citation key: {}", bibEntry);
return new SimpleJson(0, "");
}

long id = Hashing.sha256().hashString(bibEntry.getCitationKey().get(), StandardCharsets.UTF_8).asLong();
String citationKey = bibEntry.getCitationKey().get();
return new SimpleJson(id, citationKey);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@

@Path("libraries/{id}")
public class LibraryResource {
public static final Logger LOGGER = LoggerFactory.getLogger(LibraryResource.class);
private static final Logger LOGGER = LoggerFactory.getLogger(LibraryResource.class);

@Inject
CliPreferences preferences;
Expand Down
2 changes: 2 additions & 0 deletions jabsrv/src/main/java/org/jabref/http/server/Server.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import org.jabref.http.dto.GlobalExceptionMapper;
import org.jabref.http.dto.GsonFactory;
import org.jabref.http.server.cayw.CAYWResource;
import org.jabref.http.server.cayw.format.FormatterService;
import org.jabref.http.server.services.FilesToServe;
import org.jabref.logic.os.OS;

Expand Down Expand Up @@ -71,6 +72,7 @@ private HttpServer startServer(FilesToServe filesToServe, URI uri) {
ServiceLocator serviceLocator = ServiceLocatorUtilities.createAndPopulateServiceLocator();
ServiceLocatorUtilities.addFactoryConstants(serviceLocator, new GsonFactory());
ServiceLocatorUtilities.addFactoryConstants(serviceLocator, new PreferencesFactory());
ServiceLocatorUtilities.addOneConstant(serviceLocator, new FormatterService());
ServiceLocatorUtilities.addOneConstant(serviceLocator, filesToServe);

final HttpServer httpServer = startServer(serviceLocator, uri);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package org.jabref.http.server.cayw;

import java.util.Optional;

import jakarta.ws.rs.DefaultValue;
import jakarta.ws.rs.QueryParam;

public class CAYWQueryParams {

/// See documentation here https://retorque.re/zotero-better-bibtex/citing/cayw/index.html#diy

@QueryParam("probe")
private String probe;

@QueryParam("format")
@DefaultValue("biblatex")
private String format;

@QueryParam("clipboard")
private String clipboard;

@QueryParam("command")
@DefaultValue("autocite")
private String command;

@QueryParam("minimize")
private String minimize;

@QueryParam("texstudio")
private String texstudio;

@QueryParam("selected")
private String selected;

@QueryParam("select")
private String select;

@QueryParam("librarypath")
private String libraryPath;

public String getCommand() {
return command;
}

public boolean isClipboard() {
return clipboard != null;
}

public boolean isTexstudio() {
return texstudio != null;
}

public boolean isSelected() {
return selected != null;
}

public boolean isSelect() {
return select != null && select.equalsIgnoreCase("true");
}

public boolean isProbe() {
return probe != null;
}

public boolean isMinimize() {
return minimize != null;
}

public String getFormat() {
return format;
}

public Optional<String> getLibraryPath() {
return Optional.ofNullable(libraryPath);
}
}
120 changes: 66 additions & 54 deletions jabsrv/src/main/java/org/jabref/http/server/cayw/CAYWResource.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package org.jabref.http.server.cayw;

import java.awt.Toolkit;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.StringSelection;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
Expand All @@ -8,18 +11,17 @@
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.format.FormatterService;
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;
Expand All @@ -28,11 +30,10 @@

import com.google.gson.Gson;
import jakarta.inject.Inject;
import jakarta.ws.rs.DefaultValue;
import jakarta.ws.rs.BeanParam;
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;
Expand All @@ -41,7 +42,7 @@

@Path("better-bibtex/cayw")
public class CAYWResource {
public static final Logger LOGGER = LoggerFactory.getLogger(CAYWResource.class);
private static final Logger LOGGER = LoggerFactory.getLogger(CAYWResource.class);
private static final String CHOCOLATEBIB_PATH = "/Chocolate.bib";
private static boolean initialized = false;

Expand All @@ -51,23 +52,29 @@ public class CAYWResource {
@Inject
private Gson gson;

@Inject
private FormatterService formatterService;

@GET
@Produces(MediaType.TEXT_PLAIN)
public Response getCitation(
@QueryParam("probe") String probe,
@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
@BeanParam CAYWQueryParams queryParams
) throws IOException, ExecutionException, InterruptedException {
if (probe != null && !probe.isEmpty()) {
if (queryParams.isProbe()) {
return Response.ok("ready").build();
}

BibDatabaseContext databaseContext = getBibDatabaseContext(libraryPath);
BibDatabaseContext databaseContext;

// handle library path parameter
if (queryParams.getLibraryPath().isPresent() && queryParams.getLibraryPath().get().equalsIgnoreCase("demo")) {
databaseContext = getDatabaseContextFromStream(getChocolateBibAsStream());
} else if (queryParams.getLibraryPath().isPresent()) {
InputStream inputStream = getDatabaseStreamFromPath(java.nio.file.Path.of(queryParams.getLibraryPath().get()));
databaseContext = getDatabaseContextFromStream(inputStream);
} else {
databaseContext = getDatabaseContextFromStream(getLatestDatabaseStream());
}

/* unused until DatabaseSearcher is fixed
PostgreServer postgreServer = new PostgreServer();
Expand All @@ -79,71 +86,76 @@ public Response getCitation(
postgreServer);
*/

List<CAYWEntry<BibEntry>> entries = databaseContext.getEntries()
List<CAYWEntry> entries = databaseContext.getEntries()
.stream()
.map(this::createCAYWEntry)
.toList();

initializeGUI();

CompletableFuture<List<BibEntry>> future = new CompletableFuture<>();
CompletableFuture<List<CAYWEntry>> future = new CompletableFuture<>();
Platform.runLater(() -> {
SearchDialog<BibEntry> dialog = new SearchDialog<>();
SearchDialog dialog = new SearchDialog();
// 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(),
List<CAYWEntry> results = dialog.show(searchQuery ->
entries.stream().filter(caywEntry -> matches(caywEntry, searchQuery)).toList(),
entries);

future.complete(results);
});

List<String> citationKeys = future.get().stream()
.map(BibEntry::getCitationKey)
.filter(Optional::isPresent)
.map(Optional::get)
.toList();
List<CAYWEntry> searchResults = future.get();

if (citationKeys.isEmpty()) {
if (searchResults.isEmpty()) {
return Response.noContent().build();
}

return Response.ok(gson.toJson(citationKeys)).build();
// Format parameter handling
String response = formatterService.format(queryParams, searchResults);

// Clipboard parameter handling
if (queryParams.isClipboard()) {
Toolkit toolkit = Toolkit.getDefaultToolkit();
Clipboard systemClipboard = toolkit.getSystemClipboard();
StringSelection strSel = new StringSelection(response);
systemClipboard.setContents(strSel, null);
}

return Response.ok(response).build();
}

private BibDatabaseContext getBibDatabaseContext(String libraryPath) throws IOException {
private InputStream getLatestDatabaseStream() 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);
}
// Use the latest opened library as the default library
final List<java.nio.file.Path> lastOpenedLibraries = new ArrayList<>(preferences.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 {
// 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.");
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 {
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);
}
libraryStream = Files.newInputStream(lastOpenedLibrary);
}
}
return libraryStream;
}

private InputStream getDatabaseStreamFromPath(java.nio.file.Path path) throws IOException {
if (!Files.exists(path)) {
LOGGER.warn("The provided library path does not exist: {}. Using the default chocolate.bib.", path);
return getChocolateBibAsStream();
}
return Files.newInputStream(path);
}

private BibDatabaseContext getDatabaseContextFromStream(InputStream inputStream) throws IOException {
BibtexImporter bibtexImporter = new BibtexImporter(preferences.getImportFormatPreferences(), new DummyFileUpdateMonitor());
BibDatabaseContext databaseContext;
try (BufferedReader reader = new BufferedReader(new InputStreamReader(libraryStream, StandardCharsets.UTF_8))) {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
databaseContext = bibtexImporter.importDatabase(reader).getDatabaseContext();
}
return databaseContext;
Expand Down Expand Up @@ -172,14 +184,14 @@ private synchronized void initializeGUI() {
return BibDatabase.class.getResourceAsStream(CHOCOLATEBIB_PATH);
}

private CAYWEntry<BibEntry> createCAYWEntry(BibEntry entry) {
private CAYWEntry 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);
return new CAYWEntry(entry, label, shortLabel, description);
}

private boolean matches(CAYWEntry<BibEntry> entry, String searchText) {
private boolean matches(CAYWEntry entry, String searchText) {
if (searchText == null || searchText.isEmpty()) {
return true;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package org.jabref.http.server.cayw.format;

import java.util.List;
import java.util.stream.Collectors;

import org.jabref.http.server.cayw.CAYWQueryParams;
import org.jabref.http.server.cayw.gui.CAYWEntry;
import org.jabref.model.entry.BibEntry;

import org.jvnet.hk2.annotations.Service;

@Service
public class BibLatexFormatter implements CAYWFormatter {

@Override
public String getFormatName() {
return "biblatex";
}

@Override
public String format(CAYWQueryParams queryParams, List<CAYWEntry> caywEntries) {
String command = queryParams.getCommand();

List<BibEntry> bibEntries = caywEntries.stream()
.map(CAYWEntry::getBibEntry)
.toList();

return String.format("\\%s{%s}", command,
bibEntries.stream()
.map(entry -> entry.getCitationKey().orElse(""))
.collect(Collectors.joining(",")));
}
}
Loading