Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
4 changes: 3 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,12 @@
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;
//opens org.jabref.http.server.cayw.format to com.google.gson, org.glassfish.hk2.locator, org.glassfish.hk2.utilities;

// For ServiceLocatorUtilities.createAndPopulateServiceLocator()
requires org.glassfish.hk2.locator;
Expand Down
28 changes: 28 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,28 @@
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) {


public 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);
}
}
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);
}
}
118 changes: 65 additions & 53 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 @@ -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(",")));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package org.jabref.http.server.cayw.format;

import java.util.List;

import org.jabref.http.server.cayw.CAYWQueryParams;
import org.jabref.http.server.cayw.gui.CAYWEntry;

public interface CAYWFormatter {

String getFormatName();

String format(CAYWQueryParams caywQueryParams, List<CAYWEntry> caywEntries);
}
Loading