diff --git a/.jbang/JabSrvLauncher.java b/.jbang/JabSrvLauncher.java index 8b565d6d1ff..b1b7df9fb21 100755 --- a/.jbang/JabSrvLauncher.java +++ b/.jbang/JabSrvLauncher.java @@ -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 diff --git a/jabsrv/src/main/java/org/jabref/http/server/Server.java b/jabsrv/src/main/java/org/jabref/http/server/Server.java index 724a03e0ced..47e7d4f407e 100644 --- a/jabsrv/src/main/java/org/jabref/http/server/Server.java +++ b/jabsrv/src/main/java/org/jabref/http/server/Server.java @@ -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; @@ -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 diff --git a/jabsrv/src/main/java/org/jabref/http/server/resources/BibEntryResource.java b/jabsrv/src/main/java/org/jabref/http/server/resources/BibEntryResource.java new file mode 100644 index 00000000000..ee0a64833be --- /dev/null +++ b/jabsrv/src/main/java/org/jabref/http/server/resources/BibEntryResource.java @@ -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} + * + * @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 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()); + } +} diff --git a/jabsrv/src/main/java/org/jabref/http/server/resources/LibraryResource.java b/jabsrv/src/main/java/org/jabref/http/server/resources/LibraryResource.java index 9a91c3628ac..6f472cc08c5 100644 --- a/jabsrv/src/main/java/org/jabref/http/server/resources/LibraryResource.java +++ b/jabsrv/src/main/java/org/jabref/http/server/resources/LibraryResource.java @@ -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; @@ -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 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. @@ -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()); diff --git a/jabsrv/src/test/java/org/jabref/http/server/BibEntryResourceTest.java b/jabsrv/src/test/java/org/jabref/http/server/BibEntryResourceTest.java new file mode 100644 index 00000000000..ce7ebd20e30 --- /dev/null +++ b/jabsrv/src/test/java/org/jabref/http/server/BibEntryResourceTest.java @@ -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")); + } +} diff --git a/jabsrv/src/test/java/org/jabref/http/server/LibraryResourceTest.java b/jabsrv/src/test/java/org/jabref/http/server/LibraryResourceTest.java index 61240286388..61f7bbeaf0e 100644 --- a/jabsrv/src/test/java/org/jabref/http/server/LibraryResourceTest.java +++ b/jabsrv/src/test/java/org/jabref/http/server/LibraryResourceTest.java @@ -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 @@ -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"); } }