Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 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
64ae673
changed port for the http server
palukku Jul 1, 2025
15da4da
fix modifier order
palukku Jul 1, 2025
cd9fb2f
modernize file input
palukku Jul 1, 2025
54eef3d
add changes to CHANGELOG.md
palukku Jul 1, 2025
06a2d5a
fix small things
palukku Jul 1, 2025
70865c1
Add icon for dialog
koppor Jul 1, 2025
92dadba
Merge branch 'cayw' of github.com:palukku/jabref into cayw
koppor Jul 1, 2025
0b17fa0
Apply suggestions from code review
koppor Jul 1, 2025
4f516fc
Fix import
koppor Jul 1, 2025
889bc1b
change paths.get to path.of
palukku Jul 1, 2025
5822b8b
Merge remote-tracking branch 'origin/cayw' into cayw
palukku Jul 1, 2025
5389fdc
Polish strings
koppor Jul 1, 2025
ceebe6d
reuse limit from Stringutil and fix comments
palukku Jul 1, 2025
e3176ee
Merge remote-tracking branch 'origin/cayw' into cayw
palukku Jul 1, 2025
bd88c8a
reuse limit from Stringutil and fix comments
palukku Jul 1, 2025
f018a9a
changed modifier private
palukku Jul 1, 2025
dd2ac4f
use latest library
palukku Jul 1, 2025
d58a196
Apply suggestions from code review
koppor Jul 1, 2025
4986c3f
Merge branch 'main' into cayw
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv
- We distribute arm64 images for Linux. [#10842](https://github.com/JabRef/jabref/issues/10842)
- We added the field `monthfiled` to the default list of fields to resolve BibTeX-Strings for [#13375](https://github.com/JabRef/jabref/issues/13375)
- We added a new ID based fetcher for [EuropePMC](https://europepmc.org/). [#13389](https://github.com/JabRef/jabref/pull/13389)
- 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)

### Changed

Expand Down
2 changes: 2 additions & 0 deletions jablib/src/main/resources/l10n/JabRef_en.properties
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ Application=Application

Application\ to\ push\ entries\ to=Application to push entries to

%0\ |\ Cite\ As\ You\ Write=%0 | Cite As You Write

Apply=Apply

Assign\ the\ original\ group's\ entries\ to\ this\ group?=Assign the original group's entries to this group?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public class ServerCli implements Callable<Void> {
private String host = "localhost";

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

/**
* Starts an http server serving the last files opened in JabRef<br>
Expand Down
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
191 changes: 191 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,191 @@
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.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.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;
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<>();
Comment on lines +90 to +91
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GUI code is directly implemented in the resource class instead of being delegated to a proper logic layer in org.jabref.logic package.

// 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(Optional::isPresent)
.map(Optional::get)
.toList();

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

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

private BibDatabaseContext getBibDatabaseContext(String libraryPath) 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);
}
} 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.");
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);
}
}
}

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);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Method can return null but doesn't use Optional to explicitly handle the null case, violating the principle of not returning null from public methods.

}

private CAYWEntry<BibEntry> 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);
}

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

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

public class CAYWEntry<T> {
Copy link
Member

@InAnYan InAnYan Jul 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BTW, what about records?


private final T value;

// Used on the buttons ("chips")
private final String shortLabel;

// Used in the list
private final String label;

// Used when hovering and used as bases on the second line
private final String description;

private EventHandler<ActionEvent> onClick;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The onClick field can be null since it's not initialized in constructor and has no default value. This violates modern Java practices of avoiding null values in public methods.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't believe trag-bot gives good and/or interesting comments


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;
}
Comment on lines +44 to +46
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Method returns null when onClick is not set. Should return Optional<EventHandler> instead to make null-safety explicit in the API.


public void setOnClick(EventHandler<ActionEvent> onClick) {
this.onClick = onClick;
}
}
Loading