Skip to content
Closed
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
1 change: 1 addition & 0 deletions .jbang/JabSrvLauncher.java
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
//SOURCES ../jabsrv/src/main/java/org/jabref/http/server/command/SelectEntriesCommand.java
//SOURCES ../jabsrv/src/main/java/org/jabref/http/server/resources/LibrariesResource.java
//SOURCES ../jabsrv/src/main/java/org/jabref/http/server/resources/LibraryResource.java
//SOURCES ../jabsrv/src/main/java/org/jabref/http/server/resources/BibEntryResource.java
//SOURCES ../jabsrv/src/main/java/org/jabref/http/server/resources/MapResource.java
//SOURCES ../jabsrv/src/main/java/org/jabref/http/server/resources/RootResource.java
//SOURCES ../jabsrv/src/main/java/org/jabref/http/server/services/FilesToServe.java
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 @@ -14,6 +14,7 @@
import org.jabref.http.server.cayw.CAYWResource;
import org.jabref.http.server.cayw.format.FormatterService;
import org.jabref.http.server.command.CommandResource;
import org.jabref.http.server.resources.BibEntryResource;
import org.jabref.http.server.resources.LibrariesResource;
import org.jabref.http.server.resources.LibraryResource;
import org.jabref.http.server.resources.MapResource;
Expand Down Expand Up @@ -101,6 +102,7 @@ private HttpServer startServer(ServiceLocator serviceLocator, URI uri) {
resourceConfig.register(RootResource.class);
resourceConfig.register(LibrariesResource.class);
resourceConfig.register(LibraryResource.class);
resourceConfig.register(BibEntryResource.class);
resourceConfig.register(MapResource.class);

// Other resources
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package org.jabref.http.server.resources;

import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.util.List;
import java.util.Objects;

import org.jabref.http.JabrefMediaType;
import org.jabref.http.SrvStateManager;
import org.jabref.http.dto.BibEntryDTO;
import org.jabref.http.server.services.FilesToServe;
import org.jabref.http.server.services.ServerUtils;
import org.jabref.logic.citationstyle.JabRefItemDataProvider;
import org.jabref.logic.preferences.CliPreferences;
import org.jabref.model.database.BibDatabase;
import org.jabref.model.database.BibDatabaseContext;
import org.jabref.model.entry.BibEntryTypesManager;

import com.airhacks.afterburner.injection.Injector;
import com.google.gson.Gson;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.InternalServerErrorException;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.StreamingOutput;
import org.jspecify.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

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

@Inject
CliPreferences preferences;

@Inject
SrvStateManager srvStateManager;

@Inject
FilesToServe filesToServe;

@Inject
Gson gson;

/**
* At http://localhost:23119/libraries/{id}
Copy link
Member

Choose a reason for hiding this comment

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

I think, this is comment wrong, we are at the BibEntry resource

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thank you for your feedback! Do you mean we should create a new specific end-point for the BibEntry resource? Something like this : <http://localhost:23119/librairies/demo/entries/{id}>?

Copy link
Member

Choose a reason for hiding this comment

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

I hope you already did.

Note: It already worked like this. The only issue was that the methods handling that were nested in the library class.

*
* @param id The specified library
* @return specified library in JSON format
* @throws IOException
*/
@GET
@Produces(MediaType.APPLICATION_JSON)
public String getJson(@PathParam("id") String id) throws IOException {
BibDatabaseContext databaseContext = getDatabaseContext(id);
BibEntryTypesManager entryTypesManager = Injector.instantiateModelOrService(BibEntryTypesManager.class);
List<BibEntryDTO> list = databaseContext.getDatabase().getEntries().stream()
.peek(bibEntry -> bibEntry.getSharedBibEntryData().setSharedID(Objects.hash(bibEntry)))
.map(entry -> new BibEntryDTO(entry, databaseContext.getMode(), preferences.getFieldPreferences(), entryTypesManager))
.toList();
return gson.toJson(list);
}

@GET
@Produces(JabrefMediaType.JSON_CSL_ITEM)
public String getClsItemJson(@PathParam("id") String id) throws IOException {
BibDatabaseContext databaseContext = getDatabaseContext(id);
JabRefItemDataProvider jabRefItemDataProvider = new JabRefItemDataProvider();
jabRefItemDataProvider.setData(databaseContext, new BibEntryTypesManager());
return jabRefItemDataProvider.toJson();
}

@GET
@Produces(JabrefMediaType.BIBTEX)
public Response getBibtex(@PathParam("id") String id) {
if ("demo".equals(id)) {
StreamingOutput stream = output -> {
try (InputStream in = getChocolateBibAsStream()) {
in.transferTo(output);
}
};

return Response.ok(stream)
// org.glassfish.jersey.media would be required for a "nice" Java to create ContentDisposition; we avoid this
.header("Content-Disposition", "attachment; filename=\"Chocolate.bib\"")
.build();
}

java.nio.file.Path library = ServerUtils.getLibraryPath(id, filesToServe, srvStateManager);
String libraryAsString;
try {
libraryAsString = Files.readString(library);
} catch (IOException e) {
LOGGER.error("Could not read library {}", library, e);
throw new InternalServerErrorException("Could not read library " + library, e);
}
return Response.ok()
.header("Content-Disposition", "attachment; filename=\"" + library.getFileName() + "\"")
.entity(libraryAsString)
.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");
}

/// @param id - also "demo" for the Chocolate.bib file
private BibDatabaseContext getDatabaseContext(String id) throws IOException {
return ServerUtils.getBibDatabaseContext(id, filesToServe, srvStateManager, preferences.getImportFormatPreferences());
}
}
Original file line number Diff line number Diff line change
@@ -1,46 +1,29 @@
package org.jabref.http.server.resources;

import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

import org.jabref.http.JabrefMediaType;
import org.jabref.http.SrvStateManager;
import org.jabref.http.dto.BibEntryDTO;
import org.jabref.http.dto.LinkedPdfFileDTO;
import org.jabref.http.server.services.FilesToServe;
import org.jabref.http.server.services.ServerUtils;
import org.jabref.logic.citationstyle.JabRefItemDataProvider;
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.BibEntryTypesManager;
import org.jabref.model.entry.LinkedFile;

import com.airhacks.afterburner.injection.Injector;
import com.google.gson.Gson;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.InternalServerErrorException;
import jakarta.ws.rs.NotFoundException;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.StreamingOutput;
import org.jspecify.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

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

@Inject
CliPreferences preferences;

Expand All @@ -60,56 +43,6 @@ public class LibraryResource {
* @return specified library in JSON format
* @throws IOException
*/
@GET
@Produces(MediaType.APPLICATION_JSON)
public String getJson(@PathParam("id") String id) throws IOException {
BibDatabaseContext databaseContext = getDatabaseContext(id);
BibEntryTypesManager entryTypesManager = Injector.instantiateModelOrService(BibEntryTypesManager.class);
List<BibEntryDTO> list = databaseContext.getDatabase().getEntries().stream()
.peek(bibEntry -> bibEntry.getSharedBibEntryData().setSharedID(Objects.hash(bibEntry)))
.map(entry -> new BibEntryDTO(entry, databaseContext.getMode(), preferences.getFieldPreferences(), entryTypesManager))
.toList();
return gson.toJson(list);
}

@GET
@Produces(JabrefMediaType.JSON_CSL_ITEM)
public String getClsItemJson(@PathParam("id") String id) throws IOException {
BibDatabaseContext databaseContext = getDatabaseContext(id);
JabRefItemDataProvider jabRefItemDataProvider = new JabRefItemDataProvider();
jabRefItemDataProvider.setData(databaseContext, new BibEntryTypesManager());
return jabRefItemDataProvider.toJson();
}

@GET
@Produces(JabrefMediaType.BIBTEX)
public Response getBibtex(@PathParam("id") String id) {
if ("demo".equals(id)) {
StreamingOutput stream = output -> {
try (InputStream in = getChocolateBibAsStream()) {
in.transferTo(output);
}
};

return Response.ok(stream)
// org.glassfish.jersey.media would be required for a "nice" Java to create ContentDisposition; we avoid this
.header("Content-Disposition", "attachment; filename=\"Chocolate.bib\"")
.build();
}

java.nio.file.Path library = ServerUtils.getLibraryPath(id, filesToServe, srvStateManager);
String libraryAsString;
try {
libraryAsString = Files.readString(library);
} catch (IOException e) {
LOGGER.error("Could not read library {}", library, e);
throw new InternalServerErrorException("Could not read library " + library, e);
}
return Response.ok()
.header("Content-Disposition", "attachment; filename=\"" + library.getFileName() + "\"")
.entity(libraryAsString)
.build();
}

/// Loops through all entries in the specified library and adds attached files of type "PDF" to
/// a list and JSON serialises it.
Expand Down Expand Up @@ -144,13 +77,6 @@ public String getPDFFilesAsList(@PathParam("id") String id) throws IOException {
return gson.toJson(response);
}

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

/// @param id - also "demo" for the Chocolate.bib file
private BibDatabaseContext getDatabaseContext(String id) throws IOException {
return ServerUtils.getBibDatabaseContext(id, filesToServe, srvStateManager, preferences.getImportFormatPreferences());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package org.jabref.http.server;

import org.jabref.http.JabrefMediaType;
import org.jabref.http.server.resources.BibEntryResource;

import jakarta.ws.rs.core.Application;
import org.glassfish.jersey.server.ResourceConfig;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

class BibEntryResourceTest extends ServerTest {

@Override
protected Application configure() {
ResourceConfig resourceConfig = new ResourceConfig(BibEntryResource.class);
addFilesToServeToResourceConfig(resourceConfig);
addGuiBridgeToResourceConfig(resourceConfig);
addPreferencesToResourceConfig(resourceConfig);
addGsonToResourceConfig(resourceConfig);
return resourceConfig.getApplication();
}

@Test
void getJson() {
String response = target("/libraries/" + TestBibFile.GENERAL_SERVER_TEST.id).request().get(String.class);
// Basic sanity check: response is a JSON array and contains the entry id
assertTrue(response.trim().startsWith("["));
assertTrue(response.contains("\"Author2023test\""));
}

@Test
void getCslItemJson() {
String response = target("/libraries/" + TestBibFile.GENERAL_SERVER_TEST.id).request(JabrefMediaType.JSON_CSL_ITEM).get(String.class);
assertEquals("""
[{"id":"Author2023test","type":"article","author":[{"family":"Author","given":"Demo"}],"event-date":{"date-parts":[[2023]]},"issued":{"date-parts":[[2023]]},"title":"Demo Title"}]""", response);
}

@Test
void getBibtex() {
// For non-demo libraries the test reads the test file content; this asserts we get a BibTeX-like response
String response = target("/libraries/" + TestBibFile.GENERAL_SERVER_TEST.id).request(JabrefMediaType.BIBTEX).get(String.class);
assertTrue(response.contains("@Misc{Author2023test"));
assertTrue(response.contains("jabref-meta: databaseType:bibtex"));
}
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
package org.jabref.http.server;

import org.jabref.http.JabrefMediaType;
import org.jabref.http.server.resources.LibrariesResource;
import org.jabref.http.server.resources.LibraryResource;

import jakarta.ws.rs.core.Application;
import jakarta.ws.rs.core.MediaType;
import org.glassfish.jersey.server.ResourceConfig;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;

/**
* Tests for LibraryResource which provides the /libraries/{id}/entries/pdffiles endpoint
*/
class LibraryResourceTest extends ServerTest {

@Override
Expand All @@ -19,25 +22,31 @@ protected Application configure() {
addGuiBridgeToResourceConfig(resourceConfig);
addPreferencesToResourceConfig(resourceConfig);
addGsonToResourceConfig(resourceConfig);
addGlobalExceptionMapperToResourceConfig(resourceConfig);
return resourceConfig.getApplication();
}

@Test
void getJson() {
assertEquals("""
@Misc{Author2023test,
author = {Demo Author},
title = {Demo Title},
year = {2023},
}

@Comment{jabref-meta: databaseType:bibtex;}
""", target("/libraries/" + TestBibFile.GENERAL_SERVER_TEST.id).request(JabrefMediaType.BIBTEX).get(String.class));
void getPDFFilesAsListReturnsEmptyArrayWhenNoPDFs() {
// The test library has no PDF files attached, so we expect an empty JSON array
String response = target("/libraries/" + TestBibFile.GENERAL_SERVER_TEST.id + "/entries/pdffiles")
.request(MediaType.APPLICATION_JSON)
.get(String.class);

// The API returns an empty JSON array when there are entries but no linked PDF files
assertEquals("[]", response.trim());
}

@Test
void getClsItemJson() {
assertEquals("""
[{"id":"Author2023test","type":"article","author":[{"family":"Author","given":"Demo"}],"event-date":{"date-parts":[[2023]]},"issued":{"date-parts":[[2023]]},"title":"Demo Title"}]""", target("/libraries/" + TestBibFile.GENERAL_SERVER_TEST.id).request(JabrefMediaType.JSON_CSL_ITEM).get(String.class));
void getPDFFilesAsListWithDemoLibrary() {
// The demo library (Chocolate.bib) might have PDF entries
// This tests the endpoint exists and returns valid JSON
String response = target("/libraries/demo/entries/pdffiles")
.request(MediaType.APPLICATION_JSON)
.get(String.class);

// Response should be a valid JSON array (either empty [] or with entries)
// At minimum, verify it starts with [ to indicate it's a JSON array
assertEquals('[', response.charAt(0), "Response should be a JSON array");
}
}