Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 2 additions & 0 deletions jabsrv/src/main/java/module-info.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
exports org.jabref.http.dto to com.google.gson, org.glassfish.hk2.locator;

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;

// For ServiceLocatorUtilities.createAndPopulateServiceLocator()
requires org.glassfish.hk2.locator;
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 @@ -9,6 +9,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.services.FilesToServe;
import org.jabref.logic.os.OS;

Expand Down Expand Up @@ -55,6 +56,7 @@ private HttpServer startServer(ServiceLocator serviceLocator, URI uri) {
resourceConfig.register(RootResource.class);
resourceConfig.register(LibrariesResource.class);
resourceConfig.register(LibraryResource.class);
resourceConfig.register(CAYWResource.class);
resourceConfig.register(CORSFilter.class);
resourceConfig.register(GlobalExceptionMapper.class);

Expand Down
146 changes: 146 additions & 0 deletions jabsrv/src/main/java/org/jabref/http/server/cayw/CAYWResource.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
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.util.List;
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.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 boolean initialized = false;

@Inject
private CliPreferences preferences;

@Inject
private Gson gson;

@GET
@Produces(MediaType.TEXT_PLAIN)
public Response getCitation(
@QueryParam("format") @DefaultValue("latex") String format,
@QueryParam("command") String command,
@QueryParam("brackets") @DefaultValue("1") int brackets,
@QueryParam("clipboard") String clipboard,
@QueryParam("minimize") String minimize,
@QueryParam("probe") String probe
) throws IOException, ExecutionException, InterruptedException {
if (probe != null && !probe.isEmpty()) {
return Response.ok("ready").build();
}

BibtexImporter bibtexImporter = new BibtexImporter(preferences.getImportFormatPreferences(), new DummyFileUpdateMonitor());
BibDatabaseContext databaseContext;
try (InputStream chocolateBibInputStream = getChocolateBibAsStream()) {
BufferedReader reader = new BufferedReader(new InputStreamReader(chocolateBibInputStream, StandardCharsets.UTF_8));
databaseContext = bibtexImporter.importDatabase(reader).getDatabaseContext();
}
/* 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();

// 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);
}
}

CompletableFuture<List<BibEntry>> future = new CompletableFuture<>();
Platform.runLater(() -> {
SearchDialog<BibEntry> 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(),
entries);

future.complete(results);
});

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

return Response.ok(gson.toJson(citationKeys)).build();
}

/// @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() {
return BibDatabase.class.getResourceAsStream("/Chocolate.bib");
}

private CAYWEntry<BibEntry> createCAYWEntry(BibEntry entry) {
String label = entry.getCitationKey().orElse("");
String shortLabel = entry.getCitationKey().orElse("");
String description = entry.getField(StandardField.TITLE).orElse("");
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,45 @@
package org.jabref.http.server.cayw.gui;

import javafx.event.ActionEvent;
import javafx.event.EventHandler;

public class CAYWEntry<T> {

private final T value;
private final String label;
private final String shortLabel;
private final String description;

private EventHandler<ActionEvent> onClick;

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;
}

public void setOnClick(EventHandler<ActionEvent> onClick) {
this.onClick = onClick;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package org.jabref.http.server.cayw.gui;

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

import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.collections.transformation.FilteredList;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.geometry.Rectangle2D;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.Modality;
import javafx.stage.Screen;
import javafx.stage.Stage;
import javafx.stage.StageStyle;

import org.jabref.logic.l10n.Localization;

public class SearchDialog<T> {

private final ObservableList<CAYWEntry<T>> selectedItems = FXCollections.observableArrayList();
private Stage dialogStage;

public List<T> show(Function<String, List<T>> searchFunction, List<CAYWEntry<T>> entries) {
FilteredList<CAYWEntry<T>> searchResults = new FilteredList<>(FXCollections.observableArrayList(entries));
selectedItems.clear();

dialogStage = new Stage();
dialogStage.initModality(Modality.APPLICATION_MODAL);
dialogStage.initStyle(StageStyle.DECORATED);
dialogStage.setTitle(Localization.lang("Search..."));
dialogStage.setResizable(false);

Rectangle2D screenBounds = Screen.getPrimary().getVisualBounds();
double dialogWidth = screenBounds.getWidth() * 0.5;
double dialogHeight = screenBounds.getHeight() * 0.4;

VBox mainLayout = new VBox(10);
mainLayout.setPadding(new Insets(15));
mainLayout.setAlignment(Pos.TOP_CENTER);

SearchField<T> searchField = new SearchField<>(searchResults, searchFunction);
searchField.setMaxWidth(Double.MAX_VALUE);

SearchResultContainer<T> resultContainer = new SearchResultContainer<>(searchResults, selectedItems);
resultContainer.setPrefHeight(150);

ScrollPane scrollPane = new ScrollPane(resultContainer);
scrollPane.setFitToWidth(true);
scrollPane.setFitToHeight(true);
VBox.setVgrow(scrollPane, Priority.ALWAYS);

SelectedItemsContainer<T> selectedContainer = new SelectedItemsContainer<>(selectedItems);

Button finishButton = new Button(Localization.lang("Finish Search"));
finishButton.setOnAction(event -> {
dialogStage.close();
});

mainLayout.getChildren().addAll(
searchField,
selectedContainer,
scrollPane,
finishButton
);

Scene scene = new Scene(mainLayout, dialogWidth, dialogHeight);

scene.getStylesheets().add("cayw.css");
mainLayout.getStyleClass().add("search-dialog");
scrollPane.getStyleClass().add("scroll-pane");

dialogStage.setScene(scene);

dialogStage.setX((screenBounds.getWidth() - dialogWidth) / 2);
dialogStage.setY((screenBounds.getHeight() - dialogHeight) / 2);

dialogStage.showAndWait();

return selectedItems.stream().map(CAYWEntry::getValue).collect(Collectors.toList());
}

public void close() {
if (dialogStage != null) {
dialogStage.close();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package org.jabref.http.server.cayw.gui;

import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Function;

import javafx.animation.PauseTransition;
import javafx.collections.transformation.FilteredList;
import javafx.scene.control.TextField;
import javafx.util.Duration;

public class SearchField<T> extends TextField {

public SearchField(FilteredList<CAYWEntry<T>> filteredEntries, Function<String, List<T>> filter) {
PauseTransition pause = new PauseTransition(Duration.millis(100));
textProperty().addListener((observable, oldValue, newValue) -> {
pause.setOnFinished(event -> {
Set<T> currentEntries = new HashSet<>(filter.apply(newValue));
filteredEntries.setPredicate(entry -> currentEntries.contains(entry.getValue()));
});
pause.playFromStart();
});
}
}
Loading
Loading