diff --git a/.gitattributes b/.gitattributes index 18bbb30fc6d..9a7729617c8 100644 --- a/.gitattributes +++ b/.gitattributes @@ -12,6 +12,8 @@ gradlew text eol=lf # .bib files have to be written using OS specific line endings to enable our tests working *.bib text !eol +# Exception: The files used for the http server test - they should have linux line endings +src/test/resources/org/jabref/http/server/*.bib text eol=lf # Citavi needs to be LF line ending # This overwrites the setting of "*.bib" diff --git a/build.gradle b/build.gradle index c5a92c9f959..07a682f4139 100644 --- a/build.gradle +++ b/build.gradle @@ -193,6 +193,9 @@ dependencies { implementation "org.tinylog:slf4j-tinylog:2.6.2" implementation "org.tinylog:tinylog-impl:2.6.2" + // route all requests to java.util.logging to SLF4J (which in turn routes to tinylog) + implementation 'org.slf4j:jul-to-slf4j:2.0.7' + implementation 'de.undercouch:citeproc-java:3.0.0-beta.2' // jakarta.activation is already dependency of glassfish @@ -213,6 +216,23 @@ dependencies { implementation group: 'net.harawata', name: 'appdirs', version: '1.2.1' implementation group: 'org.jooq', name: 'jool', version: '0.9.15' + // JAX-RS implemented by Jersey + // API + implementation 'jakarta.ws.rs:jakarta.ws.rs-api:3.1.0' + // Implementation of the API + implementation 'org.glassfish.jersey.core:jersey-server:3.1.1' + // injection framework + implementation 'org.glassfish.jersey.inject:jersey-hk2:3.1.3' + implementation 'org.glassfish.hk2:hk2-api:2.6.1' + // testImplementation 'org.glassfish.hk2:hk2-testing:3.0.4' + // implementation 'org.glassfish.hk2:hk2-testing-jersey:3.0.4' + // testImplementation 'org.glassfish.hk2:hk2-junitrunner:3.0.4' + // HTTP server + // implementation 'org.glassfish.jersey.containers:jersey-container-netty-http:3.1.1' + implementation 'org.glassfish.jersey.containers:jersey-container-grizzly2-http:3.1.3' + testImplementation 'org.glassfish.jersey.test-framework.providers:jersey-test-framework-provider-grizzly2:3.1.3' + // Allow objects "magically" to be mapped to JSON using GSON + // implementation 'org.glassfish.jersey.media:jersey-media-json-gson:3.1.1' testImplementation 'io.github.classgraph:classgraph:4.8.162' testImplementation 'org.junit.jupiter:junit-jupiter:5.10.0' @@ -372,6 +392,12 @@ run { 'javafx.graphics/javafx.scene' : 'org.controlsfx.controls' ] } + + if (project.hasProperty('component')){ + if (component == 'httpserver'){ + main = 'org.jabref.http.server.Server' + } + } } javadoc { @@ -410,7 +436,7 @@ testlogger { showSkipped false } -task databaseTest(type: Test) { +tasks.register('databaseTest', Test) { useJUnitPlatform { includeTags 'DatabaseTest' } @@ -422,7 +448,7 @@ task databaseTest(type: Test) { } } -task fetcherTest(type: Test) { +tasks.register('fetcherTest', Test) { useJUnitPlatform { includeTags 'FetcherTest' } @@ -434,7 +460,7 @@ task fetcherTest(type: Test) { } } -task guiTest(type: Test) { +tasks.register('guiTest', Test) { useJUnitPlatform { includeTags 'GUITest' } @@ -447,7 +473,7 @@ task guiTest(type: Test) { } // Test result tasks -task copyTestResources(type: Copy) { +tasks.register('copyTestResources', Copy) { from "${projectDir}/src/test/resources" into "${buildDir}/classes/test" } @@ -457,11 +483,13 @@ tasks.withType(Test) { reports.html.outputLocation.set(file("${reporting.baseDir}/${name}")) } -task jacocoPrepare() { +tasks.register('jacocoPrepare') { doFirst { // Ignore failures of tests - tasks.withType(Test) { - ignoreFailures = true + tasks.withType(Test).tap { + configureEach { + ignoreFailures = true + } } } } @@ -519,7 +547,7 @@ modernizer { } // Release tasks -task deleteInstallerTemp(type: Delete) { +tasks.register('deleteInstallerTemp', Delete) { delete "$buildDir/installer" } diff --git a/docs/code-howtos/http-server.md b/docs/code-howtos/http-server.md new file mode 100644 index 00000000000..0652cbe70c0 --- /dev/null +++ b/docs/code-howtos/http-server.md @@ -0,0 +1,67 @@ +--- +parent: Code Howtos +--- +# HTTP Server + +## Get SSL Working + +(Based on ) + +Howto for Windows - other operating systems work similar: + +1. As admin `choco install mkcert` +2. As admin: `mkcert -install` +3. `cd %APPDATA%\..\local\org.jabref\jabref\ssl` +4. `mkcert -pkcs12 jabref.desktop jabref localhost 127.0.0.1 ::1` +5. Rename the file to `server.p12` + +Note: If you do not do this, you get following error message: + + Could not find server key store C:\Users\USERNAME\AppData\Local\org.jabref\jabref\ssl\server.p12. + +## Start http server + +The class starting the server is `org.jabref.http.server.Server`. + +Test files to server can be passed as arguments. +If no files are passed, the last opened files are served. +If that list is also empty, the file `src/main/resources/org/jabref/http/server/http-server-demo.bib` is served. + +### Starting with gradle + +Does not work. + +Current try: + + ./gradlew run -Pcomment=httpserver + +However, there are with `ForkJoin` (discussion at https://discuss.gradle.org/t/is-it-ok-to-use-collection-parallelstream-or-other-potentially-multi-threaded-code-within-gradle-plugin-code/28003) + +Gradle output: + +``` +> Task :run +2023-04-22 11:30:59 [main] org.jabref.http.server.Server.main() +DEBUG: Libraries served: [C:\git-repositories\jabref-all\jabref\src\main\resources\org\jabref\http\server\http-server-demo.bib] +2023-04-22 11:30:59 [main] org.jabref.http.server.Server.startServer() +DEBUG: Starting server... +<============-> 92% EXECUTING [2m 27s] +> :run +``` + +IntelliJ output, if `org.jabref.http.server.Server#main` is executed: + +``` +DEBUG: Starting server... +2023-04-22 11:44:59 [ForkJoinPool.commonPool-worker-1] org.glassfish.grizzly.http.server.NetworkListener.start() +INFO: Started listener bound to [localhost:6051] +2023-04-22 11:44:59 [ForkJoinPool.commonPool-worker-1] org.glassfish.grizzly.http.server.HttpServer.start() +INFO: [HttpServer] Started. +2023-04-22 11:44:59 [ForkJoinPool.commonPool-worker-1] org.jabref.http.server.Server.lambda$startServer$4() +DEBUG: Server started. +``` + +## Developing with IntelliJ + +IntelliJ Ultimate offers a Markdown-based http-client. One has to open the file `src/test/java/org/jabref/testutils/interactive/http/rest-api.http`. +Then, there are play buttons appearing for interacting with the server. diff --git a/docs/decisions/0027-http-return-bibtex-string.md b/docs/decisions/0027-http-return-bibtex-string.md new file mode 100644 index 00000000000..29658c423a3 --- /dev/null +++ b/docs/decisions/0027-http-return-bibtex-string.md @@ -0,0 +1,53 @@ +--- +nav_order: 27 +parent: Decision Records +--- + + +# Return BibTeX string and CSL Item JSON in the API + +## Context and Problem Statement + +In the context of an http server, when a http client `GETs` a JSON data structure containing BibTeX data, which format should that have? + +## Considered Options + +* Offer both, BibTeX string and CSL JSON +* Return BibTeX as is as string +* Convert BibTeX to JSON + +## Decision Outcome + +Chosen option: "Offer both, BibTeX string and CSL JSON", because there are many browser libraries out there being able to parse BibTeX. Thus, we don't need to convert it. + +## Pros and Cons of the Options + +### Offer both, BibTeX string and CSL JSON + +- Good, because this follows "Backend for Frontend" +- Good, because Word Addin works seamless with the data provided (and does not need another dependency) +- Good, because other clients can work with BibTeX data +- Bad, because two serializations have to be kept + +### Return BibTeX as is as string + +- Good, because we don't need to think about any conversion +- Bad, because it is unclear how to ship BibTeX data where the entry is dependent on +- Bad, because client needs add additional parsing logic + +### Convert BibTeX to JSON + +More thought has to be done when converting to JSON. +There seems to be a JSON format from [@citation-js/plugin-bibtex](https://www.npmjs.com/package/@citation-js/plugin-bibtex). +We could do an additional self-made JSON format, but this increases the number of available JSON serializations for BibTeX. + +- Good, because it could flatten BibTeX data (example: `author = first # " and " # second`) +- Bad, because conversion is difficult in BibTeX special cases. For instance, if Strings are used (example: `author = first # " and " # second`) and one doesn't want to flatten ("normalize") this. + +## More Information + +Existing JavaScript BibTeX libraries: + +* [bibtex-js](https://github.com/digitalheir/bibtex-js) +* [bibtexParseJS](https://github.com/ORCID/bibtexParseJs) +* [@citation-js/plugin-bibtex](https://www.npmjs.com/package/@citation-js/plugin-bibtex) diff --git a/docs/getting-into-the-code/guidelines-for-setting-up-a-local-workspace/intellij-12-build.md b/docs/getting-into-the-code/guidelines-for-setting-up-a-local-workspace/intellij-12-build.md index e1feb9355a7..bdd8959042e 100644 --- a/docs/getting-into-the-code/guidelines-for-setting-up-a-local-workspace/intellij-12-build.md +++ b/docs/getting-into-the-code/guidelines-for-setting-up-a-local-workspace/intellij-12-build.md @@ -61,6 +61,8 @@ Then double click inside the cell "Compilation options" and enter following para ```text --add-exports=javafx.controls/com.sun.javafx.scene.control=org.jabref --add-exports=org.controlsfx.controls/impl.org.controlsfx.skin=org.jabref +--add-reads org.jabref=org.fxmisc.flowless +--add-reads org.jabref=org.apache.commons.csv ``` Press Enter to have the value really stored. diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index e286e123119..1c2be7cf4a5 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -16,6 +16,8 @@ requires com.dlsc.gemsfx; uses com.dlsc.gemsfx.TagsField; requires de.saxsys.mvvmfx; + requires reactfx; + requires org.fxmisc.flowless; requires org.kordamp.ikonli.core; requires org.kordamp.ikonli.javafx; @@ -39,6 +41,7 @@ // Logging requires org.slf4j; + requires jul.to.slf4j; requires org.tinylog.api; requires org.tinylog.api.slf4j; requires org.tinylog.impl; @@ -49,45 +52,57 @@ // Preferences and XML requires java.prefs; + + // Annotations (@PostConstruct) + requires jakarta.annotation; + requires jakarta.inject; + + // http server and client exchange + requires java.net.http; + requires jakarta.ws.rs; + requires grizzly.framework; + + // data mapping requires jakarta.xml.bind; + requires jdk.xml.dom; + requires com.google.gson; + requires com.fasterxml.jackson.databind; + requires com.fasterxml.jackson.dataformat.yaml; + requires com.fasterxml.jackson.datatype.jsr310; // needs to be loaded here as it's otherwise not found at runtime requires org.glassfish.jaxb.runtime; - requires jdk.xml.dom; - // Annotations (@PostConstruct) - requires jakarta.annotation; + // dependency injection using HK2 + requires org.glassfish.hk2.api; // Microsoft application insights requires applicationinsights.core; requires applicationinsights.logging.log4j2; - // Libre Office - requires org.libreoffice.uno; - - // Other modules - requires com.google.common; - requires jakarta.inject; - requires reactfx; - requires commons.cli; - requires com.github.tomtung.latex2unicode; - requires fastparse; - requires jbibtex; - requires citeproc.java; - requires de.saxsys.mvvmfx.validation; - requires com.google.gson; + // http clients requires unirest.java; requires org.apache.httpcomponents.httpclient; requires org.jsoup; - requires org.apache.commons.csv; - requires io.github.javadiffutils; - requires java.string.similarity; + + // SQL databases requires ojdbc10; requires org.postgresql.jdbc; requires org.mariadb.jdbc; uses org.mariadb.jdbc.credential.CredentialPlugin; + + // Apache Commons and other (similar) helper libraries + requires commons.cli; + requires org.apache.commons.csv; requires org.apache.commons.lang3; - requires org.antlr.antlr4.runtime; - requires org.fxmisc.flowless; + requires com.google.common; + requires io.github.javadiffutils; + requires java.string.similarity; + + requires com.github.tomtung.latex2unicode; + requires fastparse; + + requires jbibtex; + requires citeproc.java; requires org.apache.pdfbox; requires org.apache.xmpbox; @@ -113,9 +128,6 @@ requires org.apache.lucene.analysis.common; requires org.apache.lucene.highlighter; - requires com.fasterxml.jackson.databind; - requires com.fasterxml.jackson.dataformat.yaml; - requires com.fasterxml.jackson.datatype.jsr310; requires net.harawata.appdirs; requires com.sun.jna; requires com.sun.jna.platform; @@ -123,4 +135,10 @@ requires org.eclipse.jgit; uses org.eclipse.jgit.transport.SshSessionFactory; uses org.eclipse.jgit.lib.GpgSigner; + + // other libraries + requires org.antlr.antlr4.runtime; + requires org.libreoffice.uno; + requires de.saxsys.mvvmfx.validation; + } diff --git a/src/main/java/org/jabref/cli/Launcher.java b/src/main/java/org/jabref/cli/Launcher.java index bf1554f82e0..8fb00c4742a 100644 --- a/src/main/java/org/jabref/cli/Launcher.java +++ b/src/main/java/org/jabref/cli/Launcher.java @@ -31,6 +31,7 @@ import org.apache.commons.cli.ParseException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.slf4j.bridge.SLF4JBridgeHandler; import org.tinylog.configuration.Configuration; /** @@ -46,6 +47,7 @@ public class Launcher { private static boolean isDebugEnabled; public static void main(String[] args) { + routeLoggingToSlf4J(); ARGUMENTS = args; // We must configure logging as soon as possible, which is why we cannot wait for the usual @@ -103,6 +105,11 @@ public static void main(String[] args) { } } + private static void routeLoggingToSlf4J() { + SLF4JBridgeHandler.removeHandlersForRootLogger(); + SLF4JBridgeHandler.install(); + } + /** * This needs to be called as early as possible. After the first log write, it * is not possible to alter diff --git a/src/main/java/org/jabref/http/JabrefMediaType.java b/src/main/java/org/jabref/http/JabrefMediaType.java new file mode 100644 index 00000000000..b1d3598867e --- /dev/null +++ b/src/main/java/org/jabref/http/JabrefMediaType.java @@ -0,0 +1,6 @@ +package org.jabref.http; + +public class JabrefMediaType { + public static final String BIBTEX = "application/x-bibtex"; + public static final String JSON_CSL_ITEM = "application/x-bibtex-library-csl+json"; +} diff --git a/src/main/java/org/jabref/http/dto/BibEntryDTO.java b/src/main/java/org/jabref/http/dto/BibEntryDTO.java new file mode 100644 index 00000000000..1e36564072d --- /dev/null +++ b/src/main/java/org/jabref/http/dto/BibEntryDTO.java @@ -0,0 +1,71 @@ +package org.jabref.http.dto; + +import java.io.IOException; +import java.io.StringWriter; + +import org.jabref.logic.bibtex.BibEntryWriter; +import org.jabref.logic.bibtex.FieldPreferences; +import org.jabref.logic.bibtex.FieldWriter; +import org.jabref.logic.exporter.BibWriter; +import org.jabref.model.database.BibDatabaseMode; +import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.BibEntryTypesManager; +import org.jabref.model.entry.SharedBibEntryData; + +import com.google.common.base.MoreObjects; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The data transfer object (DTO) for an BibEntry + * + * @param sharingMetadata the data used for sharing + * @param userComments the comments before the BibTeX entry + * @param citationKey the citation key (duplicated from BibEntry to ease processing by the client) + * @param bibtex the BibEntry as BibTeX string (see ADR-0027 for more information, why we don't use a HashMap / JSON) + */ +public record BibEntryDTO(SharedBibEntryData sharingMetadata, String userComments, String citationKey, String bibtex) implements Comparable { + + public static final Logger LOGGER = LoggerFactory.getLogger(BibEntryDTO.class); + + public BibEntryDTO(BibEntry bibEntry, BibDatabaseMode bibDatabaseMode, FieldPreferences fieldWriterPreferences, BibEntryTypesManager bibEntryTypesManager) { + this(bibEntry.getSharedBibEntryData(), + bibEntry.getUserComments(), + bibEntry.getCitationKey().orElse(""), + convertToString(bibEntry, bibDatabaseMode, fieldWriterPreferences, bibEntryTypesManager) + ); + } + + private static String convertToString(BibEntry entry, BibDatabaseMode bibDatabaseMode, FieldPreferences fieldWriterPreferences, BibEntryTypesManager bibEntryTypesManager) { + StringWriter rawEntry = new StringWriter(); + BibWriter bibWriter = new BibWriter(rawEntry, "\n"); + BibEntryWriter bibtexEntryWriter = new BibEntryWriter(new FieldWriter(fieldWriterPreferences), bibEntryTypesManager); + try { + bibtexEntryWriter.write(entry, bibWriter, bibDatabaseMode); + } catch (IOException e) { + LOGGER.warn("Problem creating BibTeX entry.", e); + return "error"; + } + return rawEntry.toString(); + } + + @Override + public int compareTo(BibEntryDTO o) { + int sharingComparison = sharingMetadata.compareTo(o.sharingMetadata); + if (sharingComparison != 0) { + return sharingComparison; + } + LOGGER.error("Comparing equal DTOs"); + return 0; + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("sharingMetadata", sharingMetadata) + .add("userComments", userComments) + .add("citationkey", citationKey) + .add("bibtex", bibtex) + .toString(); + } +} diff --git a/src/main/java/org/jabref/http/dto/GsonFactory.java b/src/main/java/org/jabref/http/dto/GsonFactory.java new file mode 100644 index 00000000000..77da67f9ba4 --- /dev/null +++ b/src/main/java/org/jabref/http/dto/GsonFactory.java @@ -0,0 +1,18 @@ +package org.jabref.http.dto; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import org.glassfish.hk2.api.Factory; + +public class GsonFactory implements Factory { + @Override + public Gson provide() { + return new GsonBuilder() + .setPrettyPrinting() + .create(); + } + + @Override + public void dispose(Gson instance) { + } +} diff --git a/src/main/java/org/jabref/http/server/Application.java b/src/main/java/org/jabref/http/server/Application.java new file mode 100644 index 00000000000..00567335edb --- /dev/null +++ b/src/main/java/org/jabref/http/server/Application.java @@ -0,0 +1,32 @@ +package org.jabref.http.server; + +import java.util.Set; + +import org.jabref.http.dto.GsonFactory; +import org.jabref.preferences.PreferenceServiceFactory; + +import jakarta.inject.Inject; +import jakarta.ws.rs.ApplicationPath; +import org.glassfish.hk2.api.ServiceLocator; +import org.glassfish.hk2.utilities.ServiceLocatorUtilities; + +@ApplicationPath("/") +public class Application extends jakarta.ws.rs.core.Application { + + @Inject + ServiceLocator serviceLocator; + + @Override + public Set> getClasses() { + initialize(); + return Set.of(RootResource.class, LibrariesResource.class, LibraryResource.class, CORSFilter.class); + } + + /** + * Separate initialization method, because @Inject does not support injection at the constructor + */ + private void initialize() { + ServiceLocatorUtilities.addFactoryConstants(serviceLocator, new GsonFactory()); + ServiceLocatorUtilities.addFactoryConstants(serviceLocator, new PreferenceServiceFactory()); + } +} diff --git a/src/main/java/org/jabref/http/server/CORSFilter.java b/src/main/java/org/jabref/http/server/CORSFilter.java new file mode 100644 index 00000000000..7489305808f --- /dev/null +++ b/src/main/java/org/jabref/http/server/CORSFilter.java @@ -0,0 +1,23 @@ +package org.jabref.http.server; + +import jakarta.ws.rs.container.ContainerRequestContext; +import jakarta.ws.rs.container.ContainerResponseContext; +import jakarta.ws.rs.container.ContainerResponseFilter; +import jakarta.ws.rs.ext.Provider; + +@Provider +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-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/src/main/java/org/jabref/http/server/LibrariesResource.java b/src/main/java/org/jabref/http/server/LibrariesResource.java new file mode 100644 index 00000000000..a5ea28c0ee1 --- /dev/null +++ b/src/main/java/org/jabref/http/server/LibrariesResource.java @@ -0,0 +1,29 @@ +package org.jabref.http.server; + +import java.util.List; + +import org.jabref.logic.util.io.BackupFileUtil; +import org.jabref.preferences.PreferencesService; + +import com.google.gson.Gson; +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +@Path("libraries") +public class LibrariesResource { + @Inject + PreferencesService preferences; + + @GET + @Produces(MediaType.APPLICATION_JSON) + public String get() { + List fileNamesWithUniqueSuffix = preferences.getGuiPreferences().getLastFilesOpened().stream() + .map(java.nio.file.Path::of) + .map(p -> p.getFileName() + "-" + BackupFileUtil.getUniqueFilePrefix(p)) + .toList(); + return new Gson().toJson(fileNamesWithUniqueSuffix); + } +} diff --git a/src/main/java/org/jabref/http/server/LibraryResource.java b/src/main/java/org/jabref/http/server/LibraryResource.java new file mode 100644 index 00000000000..2ff4b970723 --- /dev/null +++ b/src/main/java/org/jabref/http/server/LibraryResource.java @@ -0,0 +1,98 @@ +package org.jabref.http.server; + +import java.io.IOException; +import java.nio.file.Files; +import java.util.List; +import java.util.Objects; + +import org.jabref.gui.Globals; +import org.jabref.http.JabrefMediaType; +import org.jabref.http.dto.BibEntryDTO; +import org.jabref.logic.citationstyle.JabRefItemDataProvider; +import org.jabref.logic.importer.ParserResult; +import org.jabref.logic.importer.fileformat.BibtexImporter; +import org.jabref.logic.util.io.BackupFileUtil; +import org.jabref.model.entry.BibEntryTypesManager; +import org.jabref.model.util.DummyFileUpdateMonitor; +import org.jabref.preferences.PreferencesService; + +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 org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@Path("libraries/{id}") +public class LibraryResource { + public static final Logger LOGGER = LoggerFactory.getLogger(LibraryResource.class); + + @Inject + PreferencesService preferences; + + @Inject + Gson gson; + + @GET + @Produces(MediaType.APPLICATION_JSON) + public String getJson(@PathParam("id") String id) { + ParserResult parserResult = getParserResult(id); + List list = parserResult.getDatabase().getEntries().stream() + .peek(bibEntry -> bibEntry.getSharedBibEntryData().setSharedID(Objects.hash(bibEntry))) + .map(entry -> new BibEntryDTO(entry, parserResult.getDatabaseContext().getMode(), preferences.getFieldPreferences(), Globals.entryTypesManager)) + .toList(); + return gson.toJson(list); + } + + @GET + @Produces(JabrefMediaType.JSON_CSL_ITEM) + public String getClsItemJson(@PathParam("id") String id) { + ParserResult parserResult = getParserResult(id); + JabRefItemDataProvider jabRefItemDataProvider = new JabRefItemDataProvider(); + jabRefItemDataProvider.setData(parserResult.getDatabaseContext(), new BibEntryTypesManager()); + return jabRefItemDataProvider.toJson(); + } + + private ParserResult getParserResult(String id) { + java.nio.file.Path library = getLibraryPath(id); + ParserResult parserResult; + try { + parserResult = new BibtexImporter(preferences.getImportFormatPreferences(), new DummyFileUpdateMonitor()).importDatabase(library); + } catch (IOException e) { + LOGGER.warn("Could not find open library file {}", library, e); + throw new InternalServerErrorException("Could not parse library", e); + } + return parserResult; + } + + @GET + @Produces(JabrefMediaType.BIBTEX) + public Response getBibtex(@PathParam("id") String id) { + java.nio.file.Path library = getLibraryPath(id); + 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() + .entity(libraryAsString) + .build(); + } + + private java.nio.file.Path getLibraryPath(String id) { + return preferences.getGuiPreferences().getLastFilesOpened() + .stream() + .map(java.nio.file.Path::of) + .filter(p -> (p.getFileName() + "-" + BackupFileUtil.getUniqueFilePrefix(p)).equals(id)) + .findAny() + .orElseThrow(NotFoundException::new); + } +} diff --git a/src/main/java/org/jabref/http/server/RootResource.java b/src/main/java/org/jabref/http/server/RootResource.java new file mode 100644 index 00000000000..c55f2583db3 --- /dev/null +++ b/src/main/java/org/jabref/http/server/RootResource.java @@ -0,0 +1,22 @@ +package org.jabref.http.server; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +@Path("/") +public class RootResource { + @GET + @Produces(MediaType.TEXT_HTML) + public String get() { + return """ + + +

+ JabRef http API runs. Please navigate to libraries. +

+ +"""; + } +} diff --git a/src/main/java/org/jabref/http/server/Server.java b/src/main/java/org/jabref/http/server/Server.java new file mode 100644 index 00000000000..e238dceb577 --- /dev/null +++ b/src/main/java/org/jabref/http/server/Server.java @@ -0,0 +1,123 @@ +package org.jabref.http.server; + +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import javax.net.ssl.SSLContext; + +import javafx.collections.ObservableList; + +import org.jabref.architecture.AllowedToUseStandardStreams; +import org.jabref.logic.util.OS; +import org.jabref.preferences.JabRefPreferences; + +import jakarta.ws.rs.SeBootstrap; +import net.harawata.appdirs.AppDirsFactory; +import org.glassfish.grizzly.ssl.SSLContextConfigurator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.slf4j.bridge.SLF4JBridgeHandler; + +@AllowedToUseStandardStreams("This is a CLI application. It resides in the package http.server to be close to the other http server related classes.") +public class Server { + private static final Logger LOGGER = LoggerFactory.getLogger(Server.class); + + private static SeBootstrap.Instance serverInstance; + + /** + * Starts an http server serving the last files opened in JabRef
+ * More files can be provided as args. + */ + public static void main(final String[] args) throws InterruptedException, URISyntaxException { + SLF4JBridgeHandler.removeHandlersForRootLogger(); + SLF4JBridgeHandler.install(); + + final ObservableList lastFilesOpened = JabRefPreferences.getInstance().getGuiPreferences().getLastFilesOpened(); + + // The server serves the last opened files (see org.jabref.http.server.LibraryResource.getLibraryPath) + // In a testing environment, this might be difficult to handle + // This is a quick solution. The architectural fine solution would use some http context or other @Inject_ed variables in org.jabref.http.server.LibraryResource + if (args.length > 0) { + LOGGER.debug("Command line parameters passed"); + List filesToAdd = Arrays.stream(args) + .map(Path::of) + .filter(Files::exists) + .map(Path::toString) + .filter(path -> !lastFilesOpened.contains(path)) + .toList(); + + LOGGER.debug("Adding following files to the list of opened libraries: {}", filesToAdd); + + // add the files in the front of the last opened libraries + Collections.reverse(filesToAdd); + for (String path : filesToAdd) { + lastFilesOpened.add(0, path); + } + } + + if (lastFilesOpened.isEmpty()) { + LOGGER.debug("still no library available to serve, serve the demo library"); + // Server.class.getResource("...") is always null here, thus trying relative path + // Path bibPath = Path.of(Server.class.getResource("http-server-demo.bib").toURI()); + Path bibPath = Path.of("src/main/resources/org/jabref/http/server/http-server-demo.bib").toAbsolutePath(); + LOGGER.debug("Location of demo library: {}", bibPath); + lastFilesOpened.add(bibPath.toString()); + } + + LOGGER.debug("Libraries served: {}", lastFilesOpened); + + Server.startServer(); + + // Keep the http server running until user kills the process (e.g., presses Ctrl+C) + Thread.currentThread().join(); + } + + private static void startServer() { + SSLContext sslContext = getSslContext(); + SeBootstrap.Configuration configuration = SeBootstrap.Configuration + .builder() + .sslContext(sslContext) + .protocol("HTTPS") + .port(6051) + .build(); + LOGGER.debug("Starting server..."); + SeBootstrap.start(Application.class, configuration).thenAccept(instance -> { + LOGGER.debug("Server started."); + instance.stopOnShutdown(stopResult -> + System.out.printf("Stop result: %s [Native stop result: %s].%n", stopResult, + stopResult.unwrap(Object.class))); + final URI uri = instance.configuration().baseUri(); + System.out.printf("Instance %s running at %s [Native handle: %s].%n", instance, uri, + instance.unwrap(Object.class)); + System.out.println("Send SIGKILL to shutdown."); + serverInstance = instance; + }); + } + + private static SSLContext getSslContext() { + SSLContextConfigurator sslContextConfig = new SSLContextConfigurator(); + Path serverKeyStore = Path.of(AppDirsFactory.getInstance() + .getUserDataDir( + OS.APP_DIR_APP_NAME, + "ssl", + OS.APP_DIR_APP_AUTHOR)) + .resolve("server.p12"); + if (Files.exists(serverKeyStore)) { + sslContextConfig.setKeyStoreFile(serverKeyStore.toString()); + sslContextConfig.setKeyStorePass("changeit"); + } else { + LOGGER.error("Could not find server key store {}.", serverKeyStore); + LOGGER.error("One create one by following the steps described in [http-server.md](/docs/code-howtos/http-server.md), which is rendered at "); + } + return sslContextConfig.createSSLContext(); + } + + static void stopServer() { + serverInstance.stop(); + } +} diff --git a/src/main/java/org/jabref/logic/citationstyle/JabRefItemDataProvider.java b/src/main/java/org/jabref/logic/citationstyle/JabRefItemDataProvider.java index e75b4e7b3a9..2310a7c6b25 100644 --- a/src/main/java/org/jabref/logic/citationstyle/JabRefItemDataProvider.java +++ b/src/main/java/org/jabref/logic/citationstyle/JabRefItemDataProvider.java @@ -7,6 +7,7 @@ import java.util.Locale; import java.util.Optional; import java.util.Set; +import java.util.stream.Collectors; import org.jabref.logic.formatter.bibtexfields.RemoveNewlinesFormatter; import org.jabref.logic.integrity.PagesChecker; @@ -199,4 +200,14 @@ public Collection getIds() { .map(entry -> entry.getCitationKey().orElse("")) .toList(); } + + public String toJson() { + List entries = bibDatabaseContext.getEntries(); + this.setData(entries, bibDatabaseContext, entryTypesManager); + return entries.stream() + .map(entry -> bibEntryToCSLItemData(entry, bibDatabaseContext, entryTypesManager)) + .map(item -> item.toJson(stringJsonBuilderFactory.createJsonBuilder())) + .map(String.class::cast) + .collect(Collectors.joining(",", "[", "]")); + } } diff --git a/src/main/java/org/jabref/logic/git/GitHandler.java b/src/main/java/org/jabref/logic/git/GitHandler.java index 498e5c5f3df..e865e9b9299 100644 --- a/src/main/java/org/jabref/logic/git/GitHandler.java +++ b/src/main/java/org/jabref/logic/git/GitHandler.java @@ -71,7 +71,7 @@ void setupGitIgnore() { FileUtil.copyFile(Path.of(this.getClass().getResource("git.gitignore").toURI()), gitignore, false); } } catch (URISyntaxException e) { - LOGGER.error("Error occurred during copying of the gitignore file into the git repository."); + LOGGER.error("Error occurred during copying of the gitignore file into the git repository.", e); } } diff --git a/src/main/java/org/jabref/logic/net/ssl/TrustStoreManager.java b/src/main/java/org/jabref/logic/net/ssl/TrustStoreManager.java index 70c0bed0659..8573c968fc6 100644 --- a/src/main/java/org/jabref/logic/net/ssl/TrustStoreManager.java +++ b/src/main/java/org/jabref/logic/net/ssl/TrustStoreManager.java @@ -139,9 +139,9 @@ public X509Certificate getCertificate(String alias) { public static void createTruststoreFileIfNotExist(Path storePath) { try { LOGGER.debug("Trust store path: {}", storePath.toAbsolutePath()); - Path storeResourcePath = Path.of(TrustStoreManager.class.getResource("/ssl/truststore.jks").toURI()); - Files.createDirectories(storePath.getParent()); if (Files.notExists(storePath)) { + Path storeResourcePath = Path.of(TrustStoreManager.class.getResource("/ssl/truststore.jks").toURI()); + Files.createDirectories(storePath.getParent()); Files.copy(storeResourcePath, storePath); } } catch (IOException e) { diff --git a/src/main/java/org/jabref/preferences/JabRefPreferences.java b/src/main/java/org/jabref/preferences/JabRefPreferences.java index 46f2aab1c96..447026bd16b 100644 --- a/src/main/java/org/jabref/preferences/JabRefPreferences.java +++ b/src/main/java/org/jabref/preferences/JabRefPreferences.java @@ -119,6 +119,8 @@ import com.github.javakeyring.Keyring; import com.github.javakeyring.PasswordAccessException; import com.tobiasdiez.easybind.EasyBind; +import jakarta.inject.Singleton; +import org.jvnet.hk2.annotations.Service; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -132,6 +134,8 @@ * There are still some similar preferences classes ({@link org.jabref.logic.openoffice.OpenOfficePreferences} and {@link org.jabref.logic.shared.prefs.SharedDatabasePreferences}) which also use * the {@code java.util.prefs} API. */ +@Singleton +@Service public class JabRefPreferences implements PreferencesService { // Push to application preferences @@ -1696,9 +1700,9 @@ public CitationKeyPatternPreferences getCitationKeyPatternPreferences() { EasyBind.listen(citationKeyPatternPreferences.shouldGenerateCiteKeysBeforeSavingProperty(), (obs, oldValue, newValue) -> putBoolean(GENERATE_KEYS_BEFORE_SAVING, newValue)); EasyBind.listen(citationKeyPatternPreferences.keySuffixProperty(), (obs, oldValue, newValue) -> { - putBoolean(KEY_GEN_ALWAYS_ADD_LETTER, newValue == CitationKeyPatternPreferences.KeySuffix.ALWAYS); - putBoolean(KEY_GEN_FIRST_LETTER_A, newValue == CitationKeyPatternPreferences.KeySuffix.SECOND_WITH_A); - }); + putBoolean(KEY_GEN_ALWAYS_ADD_LETTER, newValue == CitationKeyPatternPreferences.KeySuffix.ALWAYS); + putBoolean(KEY_GEN_FIRST_LETTER_A, newValue == CitationKeyPatternPreferences.KeySuffix.SECOND_WITH_A); + }); EasyBind.listen(citationKeyPatternPreferences.keyPatternRegexProperty(), (obs, oldValue, newValue) -> put(KEY_PATTERN_REGEX, newValue)); EasyBind.listen(citationKeyPatternPreferences.keyPatternReplacementProperty(), @@ -2185,10 +2189,10 @@ public AutoLinkPreferences getAutoLinkPreferences() { bibEntryPreferences.keywordSeparatorProperty()); EasyBind.listen(autoLinkPreferences.citationKeyDependencyProperty(), (obs, oldValue, newValue) -> { - // Starts bibtex only omitted, as it is not being saved - putBoolean(AUTOLINK_EXACT_KEY_ONLY, newValue == AutoLinkPreferences.CitationKeyDependency.EXACT); - putBoolean(AUTOLINK_USE_REG_EXP_SEARCH_KEY, newValue == AutoLinkPreferences.CitationKeyDependency.REGEX); - }); + // Starts bibtex only omitted, as it is not being saved + putBoolean(AUTOLINK_EXACT_KEY_ONLY, newValue == AutoLinkPreferences.CitationKeyDependency.EXACT); + putBoolean(AUTOLINK_USE_REG_EXP_SEARCH_KEY, newValue == AutoLinkPreferences.CitationKeyDependency.REGEX); + }); EasyBind.listen(autoLinkPreferences.askAutoNamingPdfsProperty(), (obs, oldValue, newValue) -> putBoolean(ASK_AUTO_NAMING_PDFS_AGAIN, newValue)); EasyBind.listen(autoLinkPreferences.regularExpressionProperty(), diff --git a/src/main/java/org/jabref/preferences/PreferenceServiceFactory.java b/src/main/java/org/jabref/preferences/PreferenceServiceFactory.java new file mode 100644 index 00000000000..15d323c4caf --- /dev/null +++ b/src/main/java/org/jabref/preferences/PreferenceServiceFactory.java @@ -0,0 +1,14 @@ +package org.jabref.preferences; + +import org.glassfish.hk2.api.Factory; + +public class PreferenceServiceFactory implements Factory { + @Override + public PreferencesService provide() { + return JabRefPreferences.getInstance(); + } + + @Override + public void dispose(PreferencesService instance) { + } +} diff --git a/src/main/java/org/jabref/preferences/PreferencesService.java b/src/main/java/org/jabref/preferences/PreferencesService.java index 554bcf67d71..a03123740d6 100644 --- a/src/main/java/org/jabref/preferences/PreferencesService.java +++ b/src/main/java/org/jabref/preferences/PreferencesService.java @@ -34,6 +34,9 @@ import org.jabref.logic.xmp.XmpPreferences; import org.jabref.model.entry.BibEntryTypesManager; +import org.jvnet.hk2.annotations.Contract; + +@Contract public interface PreferencesService { void clear() throws BackingStoreException; diff --git a/src/main/resources/org/jabref/http/server/http-server-demo.bib b/src/main/resources/org/jabref/http/server/http-server-demo.bib new file mode 100644 index 00000000000..f5374fef3ad --- /dev/null +++ b/src/main/resources/org/jabref/http/server/http-server-demo.bib @@ -0,0 +1,14 @@ +@InProceedings{Kopp2018, + author = {Kopp, Oliver and Armbruster, Anita and Zimmermann, Olaf}, + booktitle = {Proceedings of the 10th Central European Workshop on Services and their Composition ({ZEUS} 2018)}, + title = {Markdown Architectural Decision Records: Format and Tool Support}, + year = {2018}, + editor = {Nico Herzberg and Christoph Hochreiner and Oliver Kopp and J{\"{o}}rg Lenhard}, + pages = {55--62}, + publisher = {CEUR-WS.org}, + series = {{CEUR} Workshop Proceedings}, + volume = {2072}, + keywords = {ADR, MADR, architecture decision records, architectural decision records, Nygard}, +} + +@Comment{jabref-meta: databaseType:bibtex;} diff --git a/src/main/resources/tinylog.properties b/src/main/resources/tinylog.properties index 9eecb7934bd..cf24494f6ba 100644 --- a/src/main/resources/tinylog.properties +++ b/src/main/resources/tinylog.properties @@ -8,3 +8,5 @@ exception = strip: jdk.internal #level@org.jabref.model.entry.BibEntry = debug level@org.jabref.gui.maintable.PersistenceVisualStateTable = debug + +level@org.jabref.http.server.Server = debug diff --git a/src/test/java/org/jabref/architecture/TestArchitectureTest.java b/src/test/java/org/jabref/architecture/TestArchitectureTest.java index e9e66c3600f..81f73e5a117 100644 --- a/src/test/java/org/jabref/architecture/TestArchitectureTest.java +++ b/src/test/java/org/jabref/architecture/TestArchitectureTest.java @@ -36,6 +36,7 @@ public void testsAreIndependent(JavaClasses classes) { public void testNaming(JavaClasses classes) { classes().that().areTopLevelClasses() .and().doNotHaveFullyQualifiedName("org.jabref.benchmarks.Benchmarks") + .and().doNotHaveFullyQualifiedName("org.jabref.http.server.TestBibFile") .and().doNotHaveFullyQualifiedName("org.jabref.gui.autocompleter.AutoCompleterUtil") .and().doNotHaveFullyQualifiedName("org.jabref.gui.search.TextFlowEqualityHelper") .and().doNotHaveFullyQualifiedName("org.jabref.logic.bibtex.BibEntryAssert") diff --git a/src/test/java/org/jabref/http/server/LibrariesResourceTest.java b/src/test/java/org/jabref/http/server/LibrariesResourceTest.java new file mode 100644 index 00000000000..1ec937d6daf --- /dev/null +++ b/src/test/java/org/jabref/http/server/LibrariesResourceTest.java @@ -0,0 +1,34 @@ +package org.jabref.http.server; + +import java.util.EnumSet; +import java.util.stream.Collectors; + +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; + +class LibrariesResourceTest extends ServerTest { + + @Override + protected Application configure() { + ResourceConfig resourceConfig = new ResourceConfig(LibrariesResource.class); + addPreferencesToResourceConfig(resourceConfig); + return resourceConfig.getApplication(); + } + + @Test + void defaultOneTestLibrary() throws Exception { + assertEquals("[\"" + TestBibFile.GENERAL_SERVER_TEST.id + "\"]", target("/libraries").request().get(String.class)); + } + + @Test + void twoTestLibraries() { + EnumSet availableLibraries = EnumSet.of(TestBibFile.GENERAL_SERVER_TEST, TestBibFile.JABREF_AUTHORS); + setAvailableLibraries(availableLibraries); + // We cannot use a string constant as the path changes from OS to OS. Therefore, we need to dynamically create the expected result. + String expected = availableLibraries.stream().map(file -> file.id).collect(Collectors.joining("\",\"", "[\"", "\"]")); + assertEquals(expected, target("/libraries").request().get(String.class)); + } +} diff --git a/src/test/java/org/jabref/http/server/LibraryResourceTest.java b/src/test/java/org/jabref/http/server/LibraryResourceTest.java new file mode 100644 index 00000000000..f4d43c12b77 --- /dev/null +++ b/src/test/java/org/jabref/http/server/LibraryResourceTest.java @@ -0,0 +1,39 @@ +package org.jabref.http.server; + +import org.jabref.http.JabrefMediaType; + +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; + +class LibraryResourceTest extends ServerTest { + + @Override + protected Application configure() { + ResourceConfig resourceConfig = new ResourceConfig(LibraryResource.class, LibrariesResource.class); + addPreferencesToResourceConfig(resourceConfig); + addGsonToResourceConfig(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)); + } + + @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)); + } +} diff --git a/src/test/java/org/jabref/http/server/ServerTest.java b/src/test/java/org/jabref/http/server/ServerTest.java new file mode 100644 index 00000000000..657a340e7c3 --- /dev/null +++ b/src/test/java/org/jabref/http/server/ServerTest.java @@ -0,0 +1,98 @@ +package org.jabref.http.server; + +import java.util.EnumSet; +import java.util.List; +import java.util.stream.Collectors; + +import javafx.collections.FXCollections; + +import org.jabref.http.dto.GsonFactory; +import org.jabref.logic.bibtex.FieldPreferences; +import org.jabref.logic.importer.ImportFormatPreferences; +import org.jabref.preferences.BibEntryPreferences; +import org.jabref.preferences.GuiPreferences; +import org.jabref.preferences.PreferencesService; + +import com.google.gson.Gson; +import org.glassfish.hk2.utilities.binding.AbstractBinder; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; +import org.junit.jupiter.api.BeforeAll; +import org.slf4j.bridge.SLF4JBridgeHandler; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Abstract test class to + *
    + *
  • Initialize the JCL to SLF4J bridge
  • + *
  • Provide injection capabilities of JabRef's preferences and Gson<./li> + *
+ *

More information on testing with Jersey is available at the Jersey's testing documentation

. + */ +abstract class ServerTest extends JerseyTest { + + private static PreferencesService preferencesService; + private static GuiPreferences guiPreferences; + + @BeforeAll + static void installLoggingBridge() { + // Grizzly uses java.commons.logging, but we use TinyLog + SLF4JBridgeHandler.removeHandlersForRootLogger(); + SLF4JBridgeHandler.install(); + + initializePreferencesService(); + } + + protected void addGsonToResourceConfig(ResourceConfig resourceConfig) { + resourceConfig.register(new AbstractBinder() { + @Override + protected void configure() { + bind(new GsonFactory().provide()).to(Gson.class).ranked(2); + } + }); + } + + protected void addPreferencesToResourceConfig(ResourceConfig resourceConfig) { + resourceConfig.register(new AbstractBinder() { + @Override + protected void configure() { + bind(preferencesService).to(PreferencesService.class).ranked(2); + } + }); + } + + protected void setAvailableLibraries(EnumSet files) { + when(guiPreferences.getLastFilesOpened()).thenReturn( + FXCollections.observableArrayList( + files.stream() + .map(file -> file.path.toString()) + .collect(Collectors.toList()))); + } + + private static void initializePreferencesService() { + preferencesService = mock(PreferencesService.class); + + ImportFormatPreferences importFormatPreferences = mock(ImportFormatPreferences.class); + when(preferencesService.getImportFormatPreferences()).thenReturn(importFormatPreferences); + + BibEntryPreferences bibEntryPreferences = mock(BibEntryPreferences.class); + when(importFormatPreferences.bibEntryPreferences()).thenReturn(bibEntryPreferences); + when(bibEntryPreferences.getKeywordSeparator()).thenReturn(','); + + FieldPreferences fieldWriterPreferences = mock(FieldPreferences.class); + when(preferencesService.getFieldPreferences()).thenReturn(fieldWriterPreferences); + when(fieldWriterPreferences.shouldResolveStrings()).thenReturn(false); + + // defaults are in {@link org.jabref.preferences.JabRefPreferences.NON_WRAPPABLE_FIELDS} + FieldPreferences fieldContentFormatterPreferences = new FieldPreferences(false, List.of(), List.of()); + // used twice, once for reading and once for writing + when(importFormatPreferences.fieldPreferences()).thenReturn(fieldContentFormatterPreferences); + + guiPreferences = mock(GuiPreferences.class); + when(preferencesService.getGuiPreferences()).thenReturn(guiPreferences); + + when(guiPreferences.getLastFilesOpened()).thenReturn(FXCollections.observableArrayList(TestBibFile.GENERAL_SERVER_TEST.path.toString())); + } +} diff --git a/src/test/java/org/jabref/http/server/TestBibFile.java b/src/test/java/org/jabref/http/server/TestBibFile.java new file mode 100644 index 00000000000..4897defde32 --- /dev/null +++ b/src/test/java/org/jabref/http/server/TestBibFile.java @@ -0,0 +1,18 @@ +package org.jabref.http.server; + +import java.nio.file.Path; + +import org.jabref.logic.util.io.BackupFileUtil; + +public enum TestBibFile { + GENERAL_SERVER_TEST("src/test/resources/org/jabref/http/server/general-server-test.bib"), + JABREF_AUTHORS("src/test/resources/testbib/jabref-authors.bib"); + + public final Path path; + public final String id; + + TestBibFile(String locationInSource) { + this.path = Path.of(locationInSource).toAbsolutePath(); + this.id = path.getFileName() + "-" + BackupFileUtil.getUniqueFilePrefix(path); + } +} diff --git a/src/test/java/org/jabref/logic/citationstyle/JabRefItemDataProviderTest.java b/src/test/java/org/jabref/logic/citationstyle/JabRefItemDataProviderTest.java new file mode 100644 index 00000000000..33172c35434 --- /dev/null +++ b/src/test/java/org/jabref/logic/citationstyle/JabRefItemDataProviderTest.java @@ -0,0 +1,49 @@ +package org.jabref.logic.citationstyle; + +import java.util.List; + +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.field.StandardField; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class JabRefItemDataProviderTest { + + @Test + void toJsonOneEntry() { + BibDatabase bibDatabase = new BibDatabase(List.of( + new BibEntry() + .withCitationKey("key") + .withField(StandardField.AUTHOR, "Test Author") + )); + BibDatabaseContext bibDatabaseContext = new BibDatabaseContext(bibDatabase); + JabRefItemDataProvider jabRefItemDataProvider = new JabRefItemDataProvider(); + jabRefItemDataProvider.setData(bibDatabaseContext, new BibEntryTypesManager()); + assertEquals(""" + [{"id":"key","type":"article","author":[{"family":"Author","given":"Test"}]}]""", + jabRefItemDataProvider.toJson()); + } + + @Test + void toJsonTwoEntries() { + BibDatabase bibDatabase = new BibDatabase(List.of( + new BibEntry() + .withCitationKey("key") + .withField(StandardField.AUTHOR, "Test Author"), + new BibEntry() + .withCitationKey("key2") + .withField(StandardField.AUTHOR, "Second Author") + )); + BibDatabaseContext bibDatabaseContext = new BibDatabaseContext(bibDatabase); + JabRefItemDataProvider jabRefItemDataProvider = new JabRefItemDataProvider(); + jabRefItemDataProvider.setData(bibDatabaseContext, new BibEntryTypesManager()); + assertEquals(""" + [{"id":"key","type":"article","author":[{"family":"Author","given":"Test"}]},{"id":"key2","type":"article","author":[{"family":"Author","given":"Second"}]}]""", + jabRefItemDataProvider.toJson()); + } +} diff --git a/src/test/java/org/jabref/testutils/interactive/http/rest-api.http b/src/test/java/org/jabref/testutils/interactive/http/rest-api.http new file mode 100644 index 00000000000..4994efdd6e2 --- /dev/null +++ b/src/test/java/org/jabref/testutils/interactive/http/rest-api.http @@ -0,0 +1,35 @@ +// This file is for IntelliJ's HTTP Client, available in the Ultimate Edition + +GET https://localhost:6051 + +### + +GET https://localhost:6051/libraries + +### + +GET https://localhost:6051/libraries/notfound + +### + +// if you have checkout the JabRef code at c:\git-repositories\jabref, then this +// will show the contents of your first opened library as BibTeX + +GET https://localhost:6051/libraries/jabref-authors.bib-026bd7ec +Accept: application/x-bibtex + +### + +// 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 https://localhost:6051/libraries/jabref-authors.bib-026bd7ec +Accept: application/x-bibtex-library-csl+json + +### + +// if you have checkout the JabRef code at c:\git-repositories\jabref, then this +// will show the contents of your first opened library using json + embedded BibTeX + +GET https://localhost:6051/libraries/jabref-authors.bib-026bd7ec +Accept: application/json diff --git a/src/test/resources/org/jabref/http/server/general-server-test.bib b/src/test/resources/org/jabref/http/server/general-server-test.bib new file mode 100644 index 00000000000..c1d828316d1 --- /dev/null +++ b/src/test/resources/org/jabref/http/server/general-server-test.bib @@ -0,0 +1,7 @@ +@Misc{Author2023test, + author = {Demo Author}, + title = {Demo Title}, + year = {2023}, +} + +@Comment{jabref-meta: databaseType:bibtex;} diff --git a/src/test/resources/testbib/jabref-authors.bib b/src/test/resources/testbib/jabref-authors.bib index 365c8d05019..118df45c5b5 100644 --- a/src/test/resources/testbib/jabref-authors.bib +++ b/src/test/resources/testbib/jabref-authors.bib @@ -3003,6 +3003,19 @@ @InProceedings{SimonDietzDiezEtAl2019 priority = {prio1}, } +@InProceedings{Kopp2018, + author = {Kopp, Oliver and Armbruster, Anita and Zimmermann, Olaf}, + booktitle = {Proceedings of the 10th Central European Workshop on Services and their Composition ({ZEUS} 2018)}, + date = {2018}, + title = {Markdown Architectural Decision Records: Format and Tool Support}, + editor = {Nico Herzberg and Christoph Hochreiner and Oliver Kopp and J{\"{o}}rg Lenhard}, + pages = {55--62}, + publisher = {CEUR-WS.org}, + series = {{CEUR} Workshop Proceedings}, + volume = {2072}, + keywords = {ADR, MADR, architecture decision records, architectural decision records, Nygard}, +} + @Comment{jabref-meta: databaseType:biblatex;} @Comment{jabref-meta: grouping: