Skip to content

Commit 47f441e

Browse files
kopporclaudesubhramit
authored
Add CSL-JSON import endpoint for correct entry-type mapping (#16151)
* Add CSL-JSON import endpoint for correct entry-type mapping Extend `POST /libraries/{id}/entries` to accept CSL-JSON (`application/vnd.citationstyles.csl+json`), so browser translators (and any CSL-JSON producer) can hand items to JabRef and get the correct entry type instead of a flat @Article. Rather than a bespoke Zotero-itemType -> BibTeX map (rejected by ADR 0064), reuse the merged citation-js-based CSL mapping (`CSLItemTypeDefinitions`, PR #15946): extract the CSL-JSON -> BibEntry core from `ZoteroCitationMarkParser.toBibEntry` into a new public `parseCslJsonItems(String)` that accepts a bare CSL item or an array. `EntriesResource.addCslJson` serialises the mapped entries to BibTeX and appends them via the existing `AppendBibTeXToLibrary` command, preserving duplicate handling and group assignment. Entries carry no citation key so JabRef generates keys on import. Freshly built entries are written with reformat=true (empty parsed serialisation otherwise writes nothing). Tests: type/field mapping and array/object/blank/invalid handling in `ZoteroCitationMarkParserTest`; endpoint conversion + 400 paths in `EntriesResourceTest`. Related to #15875 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(changelog): link CSL-JSON endpoint entry to PR #16151 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Drop dead null guards in parseCslJsonItems The isJsonArray/isJsonObject guards already ensure Gson returns non-null, so the != null checks were unreachable; removing them also satisfies the no-null-check style rule. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Add CSL-JSON demo requests to import.http Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Guard CSL-JSON import against explicit null fields CSL-JSON that sets optional properties explicitly to null ("type": null, "author": null, "issued": null, "issued": {"date-parts": null}) crashed the import path with an NPE (Gson overwrites the field defaults with null, and the immutable CSL mapping tables reject a null key lookup), surfacing as HTTP 500. Normalise type/author/issued and guard date-parts before use. Add a parameterised regression test covering each null shape. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Use Markdown Javadoc for the group parameter of addCslJson Replace the traditional @param tag with a Markdown sentence, per the project's Markdown-Javadoc guideline for new docs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Table-ise the null-field test with @CsvSource Use @CsvSource with a `|` delimiter (JSON keeps its commas) so the null-field cases read as a table of input -> expected entry type, and clean up the provider imports. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Apply OpenRewrite cleanup to ZoteroCitationMarkParserTest Replace list.get(0) with list.getFirst() per SequencedCollection recipe. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Update jablib/src/main/java/org/jabref/logic/openoffice/ZoteroCitationMarkParser.java Co-authored-by: Subhramit Basu <subhramit.bb@live.in> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Subhramit Basu <subhramit.bb@live.in>
1 parent d5f9e8d commit 47f441e

6 files changed

Lines changed: 275 additions & 10 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv
1111

1212
### Added
1313

14+
- The HTTP import endpoint (`POST /libraries/{id}/entries`) now accepts CSL-JSON (`application/vnd.citationstyles.csl+json`), mapping each item to the correct entry type (e.g. conference paper, book chapter, thesis) via the citation-js-based mapping. [#16151](https://github.com/JabRef/jabref/pull/16151)
1415
- We added a new "Main" tab to the entry editor showing all fields of an entry in a single scrollable list, with one-click chips for adding optional fields and a free-form box for adding arbitrary fields. Identifiers, files and links, bibliometrics, comments, and meta fields (groups, owner, timestamps, special fields) live in collapsible sections — collapsed when empty — each offering chips for its unset fields. [#12711](https://github.com/JabRef/jabref/issues/12711)
1516
- We added auto-detection import for drag-and-dropped library files. [#15391](https://github.com/JabRef/jabref/issues/15391)
1617
- We added a preview style selection bar which shows the current preview style and allows to select a specific style without cycling through all of them. [#15820](https://github.com/JabRef/jabref/pull/15820)

jablib/src/main/java/org/jabref/logic/openoffice/ZoteroCitationMarkParser.java

Lines changed: 54 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package org.jabref.logic.openoffice;
22

33
import java.util.ArrayList;
4+
import java.util.Collections;
45
import java.util.List;
56
import java.util.Map;
67
import java.util.NoSuchElementException;
@@ -13,10 +14,11 @@
1314
import org.jabref.model.entry.Date;
1415
import org.jabref.model.entry.field.Field;
1516
import org.jabref.model.entry.field.StandardField;
16-
import org.jabref.model.entry.types.EntryType;
1717

1818
import com.google.gson.Gson;
19+
import com.google.gson.JsonElement;
1920
import com.google.gson.JsonParseException;
21+
import com.google.gson.JsonParser;
2022
import org.slf4j.Logger;
2123
import org.slf4j.LoggerFactory;
2224

@@ -52,6 +54,40 @@ public static List<BibEntry> parse(String referenceMarkName) {
5254
}
5355
}
5456

57+
/// Parses a bare CSL-JSON payload (a single CSL item object, or an array of them) into
58+
/// [BibEntry] instances, reusing the citation-js-based CSL item-type/field mapping (ADR 0064).
59+
///
60+
/// Unlike [#parse(String)] this expects the CSL items directly (as produced by a Zotero /
61+
/// citation-js CSL-JSON export), not a Zotero reference-mark wrapper. The produced entries have
62+
/// no citation key, so callers importing them let JabRef generate keys from its pattern.
63+
///
64+
/// @return the parsed list of entries or an empty list when the input is blank or not parsable CSL JSON.
65+
public static List<BibEntry> parseCslJsonItems(String cslJson) {
66+
if (StringUtil.isBlank(cslJson)) {
67+
return List.of();
68+
}
69+
70+
try {
71+
JsonElement root = JsonParser.parseString(cslJson);
72+
List<ZoteroCitationData.ItemData> items = new ArrayList<>();
73+
// The isJsonArray/isJsonObject guards ensure Gson returns a non-null result here.
74+
if (root.isJsonArray()) {
75+
Collections.addAll(items, GSON.fromJson(root, ZoteroCitationData.ItemData[].class));
76+
} else if (root.isJsonObject()) {
77+
items.add(GSON.fromJson(root, ZoteroCitationData.ItemData.class));
78+
}
79+
80+
List<BibEntry> entries = new ArrayList<>();
81+
for (ZoteroCitationData.ItemData itemData : items) {
82+
toBibEntry(itemData).ifPresent(entries::add);
83+
}
84+
return entries;
85+
} catch (JsonParseException | NumberFormatException | NoSuchElementException e) {
86+
LOGGER.debug("Could not parse CSL JSON items", e);
87+
return List.of();
88+
}
89+
}
90+
5591
private static Optional<String> extractCSLJSON(String referenceMarkName) {
5692
int jsonStart = referenceMarkName.indexOf('{');
5793
int jsonEnd = referenceMarkName.lastIndexOf('}');
@@ -63,14 +99,22 @@ private static Optional<String> extractCSLJSON(String referenceMarkName) {
6399
}
64100

65101
private static Optional<BibEntry> toBibEntry(ZoteroCitationData.CitationItemData citationItem) {
66-
ZoteroCitationData.ItemData itemData = citationItem.itemData;
67-
68-
EntryType entryType = CSLItemTypeDefinitions.getEntryType(itemData.type);
69-
BibEntry entry = new BibEntry(entryType);
70-
entry.withCitationKey("Zotero-" + (citationItem.id));
71-
setAuthors(itemData.author).ifPresent(authors -> entry.withField(StandardField.AUTHOR, authors));
72-
setDate(entry, itemData.issued);
73-
for (Map.Entry<String, Field> fieldMapping : CSLItemTypeDefinitions.getFieldMappings(itemData.type, itemData).entrySet()) {
102+
return toBibEntry(citationItem.itemData)
103+
.map(entry -> entry.withCitationKey("Zotero-" + citationItem.id));
104+
}
105+
106+
private static Optional<BibEntry> toBibEntry(ZoteroCitationData.ItemData itemData) {
107+
// Gson replaces the field defaults with null when the JSON sets these keys explicitly to
108+
// null (e.g. "type": null, "author": null, "issued": null), so normalise before use. The
109+
// CSL mapping tables are immutable maps, which reject a null key lookup with an NPE.
110+
String type = itemData.type == null ? "" : itemData.type;
111+
BibEntry entry = new BibEntry(CSLItemTypeDefinitions.getEntryType(type));
112+
List<ZoteroCitationData.AuthorData> authors = itemData.author == null ? List.of() : itemData.author;
113+
setAuthors(authors).ifPresent(value -> entry.withField(StandardField.AUTHOR, value));
114+
if (itemData.issued != null) {
115+
setDate(entry, itemData.issued);
116+
}
117+
for (Map.Entry<String, Field> fieldMapping : CSLItemTypeDefinitions.getFieldMappings(type, itemData).entrySet()) {
74118
setField(entry, fieldMapping.getValue(), itemData.getFieldValue(fieldMapping.getKey()));
75119
}
76120

@@ -92,7 +136,7 @@ private static Optional<String> setAuthors(List<ZoteroCitationData.AuthorData> a
92136
}
93137

94138
private static void setDate(BibEntry entry, ZoteroCitationData.IssuedData issuedData) {
95-
if (issuedData.dateParts.isEmpty()) {
139+
if ((issuedData.dateParts == null) || issuedData.dateParts.isEmpty()) {
96140
return;
97141
}
98142

jablib/src/test/java/org/jabref/logic/openoffice/ZoteroCitationMarkParserTest.java

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,12 @@
1515
import org.junit.jupiter.api.Test;
1616
import org.junit.jupiter.params.ParameterizedTest;
1717
import org.junit.jupiter.params.provider.Arguments;
18+
import org.junit.jupiter.params.provider.CsvSource;
1819
import org.junit.jupiter.params.provider.MethodSource;
20+
import org.junit.jupiter.params.provider.ValueSource;
1921

2022
import static org.junit.jupiter.api.Assertions.assertEquals;
23+
import static org.junit.jupiter.api.Assertions.assertTrue;
2124

2225
class ZoteroCitationMarkParserTest {
2326

@@ -340,4 +343,92 @@ private static Stream<CSLTestCase> parseCSLJSON() {
340343
)
341344
);
342345
}
346+
347+
@Test
348+
void parseCslJsonItemsAcceptsBareItemObject() {
349+
List<BibEntry> entries = ZoteroCitationMarkParser.parseCslJsonItems("""
350+
{
351+
"type": "paper-conference",
352+
"title": "Conference paper",
353+
"container-title": "Proceedings title"
354+
}
355+
""");
356+
357+
assertEquals(1, entries.size());
358+
BibEntry entry = entries.getFirst();
359+
assertEquals(StandardEntryType.InProceedings, entry.getType());
360+
assertEquals(Optional.of("Conference paper"), entry.getField(StandardField.TITLE));
361+
assertEquals(Optional.of("Proceedings title"), entry.getField(StandardField.BOOKTITLE));
362+
}
363+
364+
@Test
365+
void parseCslJsonItemsLeavesCitationKeyEmptyForImport() {
366+
List<BibEntry> entries = ZoteroCitationMarkParser.parseCslJsonItems("""
367+
{"type": "article-journal", "title": "T"}
368+
""");
369+
370+
assertEquals(Optional.empty(), entries.getFirst().getCitationKey());
371+
}
372+
373+
@Test
374+
void parseCslJsonItemsAcceptsArrayOfItems() {
375+
List<BibEntry> entries = ZoteroCitationMarkParser.parseCslJsonItems("""
376+
[
377+
{"type": "book", "title": "A book"},
378+
{"type": "thesis", "title": "A thesis"}
379+
]
380+
""");
381+
382+
assertEquals(2, entries.size());
383+
assertEquals(StandardEntryType.Book, entries.getFirst().getType());
384+
assertEquals(StandardEntryType.Thesis, entries.get(1).getType());
385+
}
386+
387+
@Test
388+
void parseCslJsonItemsMapsFieldsPerType() {
389+
List<BibEntry> entries = ZoteroCitationMarkParser.parseCslJsonItems("""
390+
{
391+
"type": "article-journal",
392+
"title": "An article",
393+
"container-title": "A journal",
394+
"author": [{"family": "Doe", "given": "Jane"}],
395+
"issued": {"date-parts": [["2021", 5]]}
396+
}
397+
""");
398+
399+
BibEntry entry = entries.getFirst();
400+
assertEquals(StandardEntryType.Article, entry.getType());
401+
assertEquals(Optional.of("A journal"), entry.getField(StandardField.JOURNALTITLE));
402+
assertEquals(Optional.of("Doe, Jane"), entry.getField(StandardField.AUTHOR));
403+
assertEquals(Optional.of("2021"), entry.getField(StandardField.YEAR));
404+
}
405+
406+
@ParameterizedTest
407+
@ValueSource(strings = {"", " ", "not json", "{ broken"})
408+
void parseCslJsonItemsReturnsEmptyForBlankOrInvalid(String input) {
409+
assertTrue(ZoteroCitationMarkParser.parseCslJsonItems(input).isEmpty());
410+
}
411+
412+
/// CSL-JSON that explicitly sets optional composite properties to null must not throw
413+
/// (Gson overwrites the field defaults with null on explicit `"key": null`, and the immutable
414+
/// CSL mapping tables reject a null key lookup). `|` is the column separator so the JSON commas
415+
/// stay intact.
416+
@ParameterizedTest
417+
@CsvSource(delimiter = '|', textBlock = """
418+
# CSL JSON | entry type
419+
{"type": "article-journal", "title": "T", "author": null} | Article
420+
{"type": "article-journal", "title": "T", "issued": null} | Article
421+
{"type": "article-journal", "title": "T", "issued": {"date-parts": null}} | Article
422+
{"type": null, "title": "T"} | Misc
423+
""")
424+
void parseCslJsonItemsHandlesExplicitNullFields(String json, String expectedEntryType) {
425+
List<BibEntry> entries = ZoteroCitationMarkParser.parseCslJsonItems(json);
426+
427+
assertEquals(1, entries.size());
428+
BibEntry entry = entries.getFirst();
429+
assertEquals(expectedEntryType, entry.getType().getDisplayName());
430+
assertEquals(Optional.of("T"), entry.getField(StandardField.TITLE));
431+
assertEquals(Optional.empty(), entry.getField(StandardField.AUTHOR));
432+
assertEquals(Optional.empty(), entry.getField(StandardField.YEAR));
433+
}
343434
}

jabsrv/src/main/java/org/jabref/http/server/resources/EntriesResource.java

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import org.jabref.logic.exporter.BibWriter;
2121
import org.jabref.logic.importer.FetcherException;
2222
import org.jabref.logic.importer.util.MediaTypes;
23+
import org.jabref.logic.openoffice.ZoteroCitationMarkParser;
2324
import org.jabref.logic.preferences.CliPreferences;
2425
import org.jabref.logic.util.strings.StringUtil;
2526
import org.jabref.model.database.BibDatabaseContext;
@@ -84,6 +85,48 @@ public void addBibtex(@PathParam("id") String id, @QueryParam("group") @Nullable
8485
.orElseGet(() -> new UiCommand.AppendBibTeXToLibrary(bibtex, group))));
8586
}
8687

88+
/// Parses CSL-JSON (a single CSL item or an array of them, as produced by a Zotero /
89+
/// citation-js CSL-JSON export) into BibTeX entries and appends them to the selected library.
90+
///
91+
/// The CSL item type and fields are mapped to Bib(La)TeX via the citation-js-based mapping
92+
/// selected in [ADR 0064](docs/decisions/0064-use-citation-js-mapping.md), so a conference paper,
93+
/// book chapter, thesis, report, etc. yields the correct entry type instead of a flat @article.
94+
/// The produced entries carry no citation key; JabRef generates keys on import.
95+
///
96+
/// The optional `group` query parameter additionally files the imported entries under a
97+
/// top-level group of that name, creating it if it does not exist.
98+
@POST
99+
@Consumes(MediaTypes.CITATIONSTYLES_JSON)
100+
public void addCslJson(@PathParam("id") String id, @QueryParam("group") @Nullable String group, String cslJson) throws IOException {
101+
if (StringUtil.isBlank(cslJson)) {
102+
throw new BadRequestException("CSL-JSON data must not be empty.");
103+
}
104+
if (!uiMessageHandler.isGuiConnected()) {
105+
throw new BadRequestException("Only possible in GUI mode.");
106+
}
107+
Optional<java.nio.file.Path> targetLibrary = resolveTargetLibrary(id);
108+
109+
List<BibEntry> entries = ZoteroCitationMarkParser.parseCslJsonItems(cslJson);
110+
if (entries.isEmpty()) {
111+
throw new BadRequestException("Could not parse any CSL-JSON item from the given data.");
112+
}
113+
114+
StringWriter rawEntries = new StringWriter();
115+
BibWriter bibWriter = new BibWriter(rawEntries, "\n");
116+
BibEntryWriter entryWriter = new BibEntryWriter(
117+
new FieldWriter(preferences.getFieldPreferences()),
118+
entryTypesManager);
119+
for (BibEntry entry : entries) {
120+
// Freshly built entries have no meaningful parsed serialization, so force a reformat
121+
// instead of writing the (empty) original source back out.
122+
entryWriter.write(entry, bibWriter, BibDatabaseMode.BIBTEX, true);
123+
}
124+
125+
uiMessageHandler.handleUiCommands(List.of(targetLibrary
126+
.map(library -> new UiCommand.AppendBibTeXToLibrary(library, rawEntries.toString(), group))
127+
.orElseGet(() -> new UiCommand.AppendBibTeXToLibrary(rawEntries.toString(), group))));
128+
}
129+
87130
/// Parses a plain-text bibliography reference into a BibTeX entry and appends it to the
88131
/// currently selected library.
89132
///

jabsrv/src/test/import.http

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,47 @@ TI - Targeted RIS import
9999
PY - 2021
100100
ER -
101101

102+
### Import CSL-JSON to the active library (`current`)
103+
104+
// Consumes application/vnd.citationstyles.csl+json. The CSL item type is mapped to the correct
105+
// entry type via the citation-js-based mapping (ADR 0064), so this becomes an @InProceedings with
106+
// booktitle = {Proc. of Test}, not a flat @article. Entries get no citation key; JabRef generates one.
107+
108+
POST http://localhost:23119/libraries/current/entries
109+
Content-Type: application/vnd.citationstyles.csl+json
110+
111+
{
112+
"type": "paper-conference",
113+
"title": "A conference paper added via CSL-JSON",
114+
"container-title": "Proc. of Test",
115+
"author": [
116+
{ "family": "Doe", "given": "Jane" }
117+
],
118+
"issued": { "date-parts": [["2021", 5]] },
119+
"DOI": "10.1000/demo"
120+
}
121+
122+
### Import a CSL-JSON array (mixed item types) to a specific library
123+
124+
// An array adds several entries in one request; each item type maps independently
125+
// (book -> @Book, chapter -> @InBook, thesis -> @Thesis with publisher -> institution).
126+
127+
POST http://localhost:23119/libraries/{{libraryId}}/entries
128+
Content-Type: application/vnd.citationstyles.csl+json
129+
130+
[
131+
{ "type": "book", "title": "A book", "publisher": "Test Press", "issued": { "date-parts": [["2020"]] } },
132+
{ "type": "chapter", "title": "A chapter", "container-title": "The book", "page": "10-20" },
133+
{ "type": "thesis", "title": "A thesis", "publisher": "Test University" }
134+
]
135+
136+
### Import CSL-JSON and also assign it to a group (created if missing)
137+
138+
POST http://localhost:23119/libraries/current/entries?group=import-test-group
139+
Content-Type: application/vnd.citationstyles.csl+json
140+
141+
{ "type": "webpage", "title": "A web page", "URL": "https://example.org" }
142+
102143
###
103144

104145
# Creation — error cases
@@ -116,6 +157,13 @@ POST http://localhost:23119/libraries/{{libraryId}}/entries
116157
Content-Type: application/x-bibtex
117158

118159

160+
### Add unparseable CSL-JSON -> 400 Bad Request
161+
162+
POST http://localhost:23119/libraries/current/entries
163+
Content-Type: application/vnd.citationstyles.csl+json
164+
165+
not json
166+
119167
## Cached plain-citation lookup, then add
120168

121169
// Two-step flow so the (possibly expensive) parse happens once:

jabsrv/src/test/java/org/jabref/http/server/EntriesResourceTest.java

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,12 @@ private Response postBibtex(String id, String bibtex) {
6262
.post(Entity.entity(bibtex, MediaTypes.APPLICATION_BIBTEX));
6363
}
6464

65+
private Response postCslJson(String id, String cslJson) {
66+
return target("/libraries/" + id + "/entries")
67+
.request()
68+
.post(Entity.entity(cslJson, MediaTypes.CITATIONSTYLES_JSON));
69+
}
70+
6571
/// Rebinds the standalone null object ([UiMessageHandler#NONE], not GUI-connected) and restarts
6672
/// the test container so the resource sees it. Mirrors {@link ServerTest#setAvailableLibraries}.
6773
private void useStandaloneMode() throws Exception {
@@ -115,9 +121,41 @@ void importEmptyBibtexReturns400() {
115121
Mockito.verifyNoInteractions(uiMessageHandler);
116122
}
117123

124+
/// CSL-JSON is mapped to the correct entry type (paper-conference -> @InProceedings) and container
125+
/// field (container-title -> booktitle) before being appended as BibTeX.
126+
@Test
127+
void importCslJsonConvertsEntryType() {
128+
Response response = postCslJson(TestBibFile.GENERAL_SERVER_TEST.id, """
129+
{"type":"paper-conference","title":"A paper","container-title":"Proc. of Test"}
130+
""");
131+
132+
assertEquals(Response.Status.NO_CONTENT.getStatusCode(), response.getStatus());
133+
UiCommand.AppendBibTeXToLibrary command = (UiCommand.AppendBibTeXToLibrary) captureSingleCommand();
134+
assertEquals(java.util.Optional.of(TestBibFile.GENERAL_SERVER_TEST.path), command.library());
135+
assertTrue(command.bibtex().toLowerCase(java.util.Locale.ROOT).contains("@inproceedings"), command.bibtex());
136+
assertTrue(command.bibtex().contains("Proc. of Test"), command.bibtex());
137+
}
138+
139+
@Test
140+
void importEmptyCslJsonReturns400() {
141+
Response response = postCslJson(TestBibFile.GENERAL_SERVER_TEST.id, " ");
142+
143+
assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus());
144+
Mockito.verifyNoInteractions(uiMessageHandler);
145+
}
146+
147+
@Test
148+
void importUnparseableCslJsonReturns400() {
149+
Response response = postCslJson(TestBibFile.GENERAL_SERVER_TEST.id, "not json");
150+
151+
assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus());
152+
Mockito.verify(uiMessageHandler, Mockito.never()).handleUiCommands(Mockito.any());
153+
}
154+
118155
static Stream<Arguments> standaloneEndpoints() {
119156
return Stream.of(
120157
Arguments.of(MediaTypes.APPLICATION_BIBTEX, "@article{a, title={t}}"),
158+
Arguments.of(MediaTypes.CITATIONSTYLES_JSON, "{\"type\":\"book\",\"title\":\"t\"}"),
121159
Arguments.of(MediaType.TEXT_PLAIN, "Smith, J. (2020). A title."),
122160
Arguments.of(MediaType.APPLICATION_OCTET_STREAM, "some data"));
123161
}

0 commit comments

Comments
 (0)