Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
ea620a5
initial cayw
palukku May 29, 2025
7d57d0e
Merge branch 'main' into cayw
palukku May 30, 2025
0877a93
fix issues mentioned by subhramit
palukku May 30, 2025
546b32a
Merge branch 'JabRef:main' into cayw
palukku May 30, 2025
9b52f2f
Merge branch 'main' into cayw
koppor Jun 2, 2025
7c2826f
Merge branch 'JabRef:main' into cayw
palukku Jun 6, 2025
4911107
Merge remote-tracking branch 'origin/main' into cayw
koppor Jun 9, 2025
795d229
Merge branch 'JabRef:main' into cayw
palukku Jun 10, 2025
fa06686
Merge branch 'JabRef:main' into cayw
palukku Jun 29, 2025
d96ed1e
Merge branch 'JabRef:main' into cayw
palukku Jul 1, 2025
18e105d
Merge branch 'JabRef:main' into cayw
palukku Jul 1, 2025
91ffba8
initial cayw
palukku May 29, 2025
2e49274
initial cayw
palukku May 29, 2025
d2da505
Merge branch 'main' into cayw
palukku May 30, 2025
b72a1ce
fix issues mentioned by subhramit
palukku May 30, 2025
015d5a3
Merge branch 'JabRef:main' into cayw
palukku May 30, 2025
2f895ec
Merge branch 'main' into cayw
koppor Jun 2, 2025
0626829
Merge branch 'JabRef:main' into cayw
palukku Jun 6, 2025
0a6743b
Merge remote-tracking branch 'origin/main' into cayw
koppor Jun 9, 2025
0825582
Merge branch 'JabRef:main' into cayw
palukku Jun 10, 2025
7e9c94f
Merge branch 'JabRef:main' into cayw
palukku Jun 29, 2025
85ef99e
Merge branch 'main' into cayw
palukku May 30, 2025
04a1fab
fix issues mentioned by subhramit
palukku May 30, 2025
62c7929
Merge branch 'JabRef:main' into cayw
palukku May 30, 2025
c3828cf
Merge branch 'main' into cayw
koppor Jun 2, 2025
edf2b71
Merge branch 'JabRef:main' into cayw
palukku Jun 6, 2025
f516650
Merge remote-tracking branch 'origin/main' into cayw
koppor Jun 9, 2025
54f3937
Merge branch 'JabRef:main' into cayw
palukku Jun 10, 2025
90e4c96
Merge branch 'JabRef:main' into cayw
palukku Jun 29, 2025
6bc30a0
Merge branch 'JabRef:main' into cayw
palukku Jul 1, 2025
22200fc
Merge branch 'JabRef:main' into cayw
palukku Jul 1, 2025
59cece2
fix minor comments and add librarypath (#3)
palukku Jul 1, 2025
99ff753
Merge remote-tracking branch 'origin/cayw' into cayw
palukku Jul 1, 2025
89d09f8
Fix submodules
palukku Jul 1, 2025
f7de70a
fix jabrefHost.py
palukku Jul 1, 2025
a9d66b2
fix jabrefHost.py
palukku Jul 1, 2025
caca932
Discard changes to jabgui/buildres/linux/jabrefHost.py
koppor Jul 1, 2025
f32016f
Discard changes to jabgui/buildres/macos/Resources/jabrefHost.py
koppor Jul 1, 2025
56eabd3
Fix mode
koppor Jul 1, 2025
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
173 changes: 173 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,173 @@
package org.jabref.http.server.cayw;

import java.io.BufferedReader;
import java.io.FileInputStream;
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 final String CHOCOLATEBIB_PATH = "/Chocolate.bib";
private static boolean initialized = false;

@Inject
private CliPreferences preferences;

@Inject
private Gson gson;

@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
) 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<>();
// 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();

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

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

private BibDatabaseContext getBibDatabaseContext(String libraryPath) throws IOException {
InputStream libraryStream;
if (libraryPath != null && !libraryPath.isEmpty()) {
libraryStream = new FileInputStream(libraryPath);
} else {
// TODO: Add a way to use latest opened library as the default library
libraryStream = getChocolateBibAsStream();
}

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() {
return BibDatabase.class.getResourceAsStream(CHOCOLATEBIB_PATH);
}

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,98 @@
package org.jabref.http.server.cayw.gui;

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

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 static final double DIALOG_WIDTH_RATIO = 0.5;
private static final double DIALOG_HEIGHT_RATIO = 0.4;
private static final int PREF_HEIGHT = 150;

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() * DIALOG_WIDTH_RATIO;
double dialogHeight = screenBounds.getHeight() * DIALOG_HEIGHT_RATIO;

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(PREF_HEIGHT);

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).toList();
}

public void close() {
if (dialogStage != null) {
dialogStage.close();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
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 {

private final static int DELAY_IN_MS = 100;

public SearchField(FilteredList<CAYWEntry<T>> filteredEntries, Function<String, List<T>> filter) {
PauseTransition pause = new PauseTransition(Duration.millis(DELAY_IN_MS));
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