diff --git a/.jbang/JabSrvLauncher.java b/.jbang/JabSrvLauncher.java index d8a040c1cab..1bb8441f444 100644 --- a/.jbang/JabSrvLauncher.java +++ b/.jbang/JabSrvLauncher.java @@ -12,6 +12,7 @@ //SOURCES ../jabsrv/src/main/java/org/jabref/http/dto/cayw/SimpleJson.java //SOURCES ../jabsrv/src/main/java/org/jabref/http/dto/GlobalExceptionMapper.java //SOURCES ../jabsrv/src/main/java/org/jabref/http/dto/GsonFactory.java +//SOURCES ../jabsrv/src/main/java/org/jabref/http/dto/LinkedPdfFileDTO.java //SOURCES ../jabsrv/src/main/java/org/jabref/http/JabrefMediaType.java //SOURCES ../jabsrv/src/main/java/org/jabref/http/server/cayw/CAYWResource.java //SOURCES ../jabsrv/src/main/java/org/jabref/http/server/cayw/CAYWQueryParams.java diff --git a/jabsrv/src/main/java/module-info.java b/jabsrv/src/main/java/module-info.java index 8487d893387..9b6281b5a9d 100644 --- a/jabsrv/src/main/java/module-info.java +++ b/jabsrv/src/main/java/module-info.java @@ -8,6 +8,7 @@ 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 com.google.gson, org.glassfish.hk2.locator, org.glassfish.hk2.utilities; + opens org.jabref.http.dto to com.google.gson; requires javafx.base; diff --git a/jabsrv/src/main/java/org/jabref/http/dto/LinkedPdfFileDTO.java b/jabsrv/src/main/java/org/jabref/http/dto/LinkedPdfFileDTO.java new file mode 100644 index 00000000000..531f72be47b --- /dev/null +++ b/jabsrv/src/main/java/org/jabref/http/dto/LinkedPdfFileDTO.java @@ -0,0 +1,28 @@ +package org.jabref.http.dto; + +import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.LinkedFile; + +public class LinkedPdfFileDTO { + private final String fileName; + private final String parentCitationKey; + private final String path; + + public LinkedPdfFileDTO(BibEntry parentEntry, LinkedFile file) { + this.parentCitationKey = parentEntry.getCitationKey().orElse("N/A"); + this.path = file.getLink(); + this.fileName = path.substring(path.lastIndexOf('/') + 1); + } + + public String getFileName() { + return fileName; + } + + public String getParentCitationKey() { + return parentCitationKey; + } + + public String getPath() { + return path; + } +} diff --git a/jabsrv/src/main/java/org/jabref/http/server/CORSFilter.java b/jabsrv/src/main/java/org/jabref/http/server/CORSFilter.java index 7489305808f..77fa155df6a 100644 --- a/jabsrv/src/main/java/org/jabref/http/server/CORSFilter.java +++ b/jabsrv/src/main/java/org/jabref/http/server/CORSFilter.java @@ -9,13 +9,7 @@ public class CORSFilter implements ContainerResponseFilter { @Override public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) { - String requestOrigin = requestContext.getHeaderString("Origin"); - if (requestOrigin == null) { - // IntelliJ's rest client is calling - responseContext.getHeaders().add("Access-Control-Allow-Origin", "*"); - } else if (requestOrigin.contains("://localhost")) { - responseContext.getHeaders().add("Access-Control-Allow-Origin", requestOrigin); - } + responseContext.getHeaders().add("Access-Control-Allow-Origin", "*"); responseContext.getHeaders().add("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS"); responseContext.getHeaders().add("Access-Control-Allow-Headers", "origin, content-type, accept"); responseContext.getHeaders().add("Access-Control-Allow-Credentials", "false"); diff --git a/jabsrv/src/main/java/org/jabref/http/server/LibraryResource.java b/jabsrv/src/main/java/org/jabref/http/server/LibraryResource.java index 9a8bcf9380e..bd704c8e454 100644 --- a/jabsrv/src/main/java/org/jabref/http/server/LibraryResource.java +++ b/jabsrv/src/main/java/org/jabref/http/server/LibraryResource.java @@ -3,11 +3,13 @@ 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.dto.BibEntryDTO; +import org.jabref.http.dto.LinkedPdfFileDTO; import org.jabref.http.server.services.ContextsToServe; import org.jabref.http.server.services.FilesToServe; import org.jabref.http.server.services.ServerUtils; @@ -15,13 +17,19 @@ 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 org.jabref.model.entry.field.StandardField; import com.airhacks.afterburner.injection.Injector; import com.google.gson.Gson; import jakarta.inject.Inject; +import jakarta.ws.rs.Consumes; import jakarta.ws.rs.GET; import jakarta.ws.rs.InternalServerErrorException; +import jakarta.ws.rs.NotFoundException; +import jakarta.ws.rs.PUT; import jakarta.ws.rs.Path; import jakarta.ws.rs.PathParam; import jakarta.ws.rs.Produces; @@ -48,18 +56,94 @@ public class LibraryResource { @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(); + .peek(bibEntry -> bibEntry.getSharedBibEntryData().setSharedID(Objects.hash(bibEntry))) + .map(entry -> new BibEntryDTO(entry, databaseContext.getMode(), preferences.getFieldPreferences(), entryTypesManager)) + .toList(); return gson.toJson(list); } + /** + * At http://localhost:23119/libraries/{id}/map

+ * + * Looks for the .jmp file in the directory of the given library ({id}.bib file). + * + * @param id The given library + * @return A JSON String containing the mindmap data. If no {id}.jmp file was found, returns the standard mindmap + * @throws IOException + */ + @GET + @Path("map") + @Produces(MediaType.APPLICATION_JSON) + public String getJabMapJson(@PathParam("id") String id) throws IOException { + boolean isDemo = "demo".equals(id); + java.nio.file.Path jabMapPath; + if (isDemo) { + jabMapPath = getJabMapDemoPath(); + } else { + jabMapPath = getJabMapPath(id); + } + // if no file is found, return the default mindmap + if (!Files.exists(jabMapPath)) { + return """ + { + "map": { + "meta": { + "name": "JabMap", + "author": "JabMap", + "version": "1.0" + }, + "format": "node_tree", + "data": { + "id": "root", + "topic": "JabMap", + "expanded": true, + "icons": [], + "highlight": null, + "type": "Text" + } + } + } + """; + } + return Files.readString(jabMapPath); + } + + /** + * At http://localhost:23119/libraries/{id}/map

+ * + * Saves the mindmap next to its associated library. + * + * @param id The given library + * + * @throws IOException + */ + @PUT + @Path("map") + @Consumes(MediaType.APPLICATION_JSON) + public void updateJabMapJson(@PathParam("id") String id, String fileContent) throws IOException { + boolean isDemo = "demo".equals(id); + java.nio.file.Path targetPath; + if (isDemo) { + targetPath = getJabMapDemoPath(); + } else { + targetPath = getJabMapPath(id); + } + Files.writeString(targetPath, fileContent); + } + @GET @Produces(JabrefMediaType.JSON_CSL_ITEM) public String getClsItemJson(@PathParam("id") String id) throws IOException { @@ -94,17 +178,165 @@ public Response getBibtex(@PathParam("id") String id) { throw new InternalServerErrorException("Could not read library " + library, e); } return Response.ok() - .header("Content-Disposition", "attachment; filename=\"" + library.getFileName() + "\"") - .entity(libraryAsString) - .build(); + .header("Content-Disposition", "attachment; filename=\"" + library.getFileName() + "\"") + .entity(libraryAsString) + .build(); + } + + private java.nio.file.Path getJabMapPath(String id) { + java.nio.file.Path libraryPath = ServerUtils.getLibraryPath(id, filesToServe, contextsToServe); + String newName = libraryPath.getFileName().toString().replaceFirst("\\.bib$", ".jmp"); + return libraryPath.getParent().resolve(newName); + } + + private java.nio.file.Path getJabMapDemoPath() { + java.nio.file.Path result = java.nio.file.Path.of(System.getProperty("java.io.tmpdir")).resolve("demo.jmp"); + LOGGER.debug("Using temporary file for demo jmp: {}", result); + return result; } - /// @param id - also "demo" for the Chocolate.bib file + /** + * @param id - also "demo" for the Chocolate.bib file + */ private BibDatabaseContext getDatabaseContext(String id) throws IOException { return ServerUtils.getBibDatabaseContext(id, filesToServe, contextsToServe, preferences.getImportFormatPreferences()); } - /// @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) + /** + * At http://localhost:23119/libraries/{id}/entries/{entryId}

+ * + * Combines attributes of a given BibEntry into a basic entry preview for as plain text. + * + * @param id The name of the library + * @param entryId The CitationKey of the BibEntry + * @return a basic entry preview as plain text + * @throws IOException + * @throws NotFoundException + */ + @GET + @Path("entries/{entryId}") + @Produces(MediaType.TEXT_PLAIN + ";charset=UTF-8") + public String getPlainRepresentation(@PathParam("id") String id, @PathParam("entryId") String entryId) throws IOException { + BibDatabaseContext databaseContext = getDatabaseContext(id); + List entriesByCitationKey = databaseContext.getDatabase().getEntriesByCitationKey(entryId); + if (entriesByCitationKey.isEmpty()) { + throw new NotFoundException("Entry with citation key '" + entryId + "' not found in library " + id); + } + if (entriesByCitationKey.size() > 1) { + LOGGER.warn("Multiple entries found with citation key '{}'. Using the first one.", entryId); + } + + // TODO: Currently, the preview preferences are in GUI package, which is not accessible here. + // build the preview + BibEntry entry = entriesByCitationKey.getFirst(); + + String author = entry.getField(StandardField.AUTHOR).orElse("(N/A)"); + String title = entry.getField(StandardField.TITLE).orElse("(N/A)"); + String journal = entry.getField(StandardField.JOURNAL).orElse("(N/A)"); + String volume = entry.getField(StandardField.VOLUME).orElse("(N/A)"); + String number = entry.getField(StandardField.NUMBER).orElse("(N/A)"); + String pages = entry.getField(StandardField.PAGES).orElse("(N/A)"); + String releaseDate = entry.getField(StandardField.DATE).orElse("(N/A)"); + + // the only difference to the HTML version of this method is the format of the output: + String preview = + "Author: " + author + + "\nTitle: " + title + + "\nJournal: " + journal + + "\nVolume: " + volume + + "\nNumber: " + number + + "\nPages: " + pages + + "\nReleased on: " + releaseDate; + + return preview; + } + + /** + * At http://localhost:23119/libraries/{id}/entries/{entryId}

+ * + * Combines attributes of a given BibEntry into a basic entry preview for as HTML text. + * + * @param id The name of the library + * @param entryId The CitationKey of the BibEntry + * @return a basic entry preview as HTML text + * @throws IOException + */ + @GET + @Path("entries/{entryId}") + @Produces(MediaType.TEXT_HTML + ";charset=UTF-8") + public String getHTMLRepresentation(@PathParam("id") String id, @PathParam("entryId") String entryId) throws IOException { + List entriesByCitationKey = getDatabaseContext(id).getDatabase().getEntriesByCitationKey(entryId); + if (entriesByCitationKey.isEmpty()) { + throw new NotFoundException("Entry with citation key '" + entryId + "' not found in library " + id); + } + if (entriesByCitationKey.size() > 1) { + LOGGER.warn("Multiple entries found with citation key '{}'. Using the first one.", entryId); + } + + // TODO: Currently, the preview preferences are in GUI package, which is not accessible here. + // build the preview + BibEntry entry = entriesByCitationKey.getFirst(); + + String author = entry.getField(StandardField.AUTHOR).orElse("(N/A)"); + String title = entry.getField(StandardField.TITLE).orElse("(N/A)"); + String journal = entry.getField(StandardField.JOURNAL).orElse("(N/A)"); + String volume = entry.getField(StandardField.VOLUME).orElse("(N/A)"); + String number = entry.getField(StandardField.NUMBER).orElse("(N/A)"); + String pages = entry.getField(StandardField.PAGES).orElse("(N/A)"); + String releaseDate = entry.getField(StandardField.DATE).orElse("(N/A)"); + + // the only difference to the plain text version of this method is the format of the output: + String preview = + "Author: " + author + "
" + + "Title: " + title + "
" + + "Journal: " + journal + "
" + + "Volume: " + volume + "
" + + "Number: " + number + "
" + + "Pages: " + pages + "
" + + "Released on: " + releaseDate; + + return preview; + } + + /** + * At http://localhost:23119/libraries/{id}/entries/pdffiles

+ * + * Loops through all entries in the specified library and adds attached files of type "PDF" to + * a list and JSON serialises it. + */ + @GET + @Path("entries/pdffiles") + @Produces(MediaType.APPLICATION_JSON + ";charset=UTF-8") + public String getPDFFilesAsList(@PathParam("id") String id) throws IOException { + // get a list of all entries in library (specified by "id") + BibDatabaseContext databaseContext = getDatabaseContext(id); + List response = new ArrayList<>(); + List entries = databaseContext.getDatabase().getEntries(); + if (entries.isEmpty()) { + throw new NotFoundException("No entries found for library: " + id); + } + + // loop through all entries to extract pdfs and paths + for (BibEntry entry : entries) { + List pathsToFiles = entry.getFiles(); + if (!pathsToFiles.isEmpty()) { + for (LinkedFile file : pathsToFiles) { + // ignore all non pdf files and online references + if (!"PDF".equals(file.getFileType()) || LinkedFile.isOnlineLink(file.getLink())) { + continue; + } + // add file to response body + LinkedPdfFileDTO localPdfFile = new LinkedPdfFileDTO(entry, file); + response.add(localPdfFile); + } + } + } + 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"); } 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 78b743c4479..26c5a71c71a 100644 --- a/jabsrv/src/main/java/org/jabref/http/server/Server.java +++ b/jabsrv/src/main/java/org/jabref/http/server/Server.java @@ -94,6 +94,7 @@ private HttpServer startServer(ServiceLocator serviceLocator, URI uri) { // see https://stackoverflow.com/a/33794265/873282 final ResourceConfig resourceConfig = new ResourceConfig(); + resourceConfig.property("jersey.config.server.wadl.disableWadl", true); // TODO: Add SSL resourceConfig.register(RootResource.class); resourceConfig.register(LibrariesResource.class); diff --git a/jabsrv/src/test/rest-api.http b/jabsrv/src/test/rest-api.http index e25f9b003ed..8799ee1c1fe 100644 --- a/jabsrv/src/test/rest-api.http +++ b/jabsrv/src/test/rest-api.http @@ -1,4 +1,4 @@ -# JabRef REST AP +# JabRef REST API // This file is for IntelliJ's HTTP Client, available in the Ultimate Edition @@ -35,14 +35,40 @@ Accept: application/json GET http://localhost:23119/libraries/demo Accept: application/x-bibtex -### Get CSL JSON of Chocolate.bib +### Get list of files in Chocolate.bib -// if you have checkout the JabRef code at c:\git-repositories\jabref, then this -// will show the contents of your first opened library using CSL JSON +GET http://localhost:23119/libraries/demo/entries/pdffiles +Accept: application/json + +### Get CSL JSON of Chocolate.bib GET http://localhost:23119/libraries/demo Accept: application/x-bibtex-library-csl+json +### Read demo JabMap (Chocolate.jmp) + +GET http://localhost:23119/libraries/demo/map +Accept: application/json + +### Save demo JabMap (Chocolate.jmp) note: this might break the data for the demo map + +PUT http://localhost:23119/libraries/demo/map +Content-Type: application/json + +{ + "name": "demo" +} + +### Get HTML-representation of entry preview for "Tokede_2011" of Chocolate.bib + +GET http://localhost:23119/libraries/demo/entries/Tokede_2011 +Accept: text/html + +### Get String-representation of entry preview for "Corti_2009" of Chocolate.bib + +GET http://localhost:23119/libraries/demo/entries/Corti_2009 +Accept: text/plain + ## From here: C:\git-repositories\JabRef\jablib\src\main\resources\Chocolate.bib is required ### Get BibTeX of C:\git-repositories\JabRef\jablib\src\main\resources\Chocolate.bib @@ -57,7 +83,7 @@ Accept: application/json ### Get CSL JSON of C:\git-repositories\JabRef\jablib\src\main\resources\Chocolate.bib -// if you have checkout the JabRef code at c:\git-repositories\jabref, then this +// if you have checked out the JabRef code at c:\git-repositories\jabref, then this // will show the contents of your first opened library using CSL JSON GET http://localhost:23119/libraries/Chocolate.bib-6a732609 @@ -65,6 +91,6 @@ Accept: application/x-bibtex-library-csl+json ## Error cases -### GET not avaialble library +### GET not available library GET http://localhost:23119/libraries/notfound