diff --git a/cometd-documentation/src/main/asciidoc/java_json.adoc b/cometd-documentation/src/main/asciidoc/java_json.adoc index 2de3bc0fb0..934067e249 100644 --- a/cometd-documentation/src/main/asciidoc/java_json.adoc +++ b/cometd-documentation/src/main/asciidoc/java_json.adoc @@ -4,13 +4,13 @@ CometD allows you to customize the JSON library that it uses to convert incoming JSON into Bayeux messages and generate JSON from Bayeux messages. -Two implementations are available, one based on Jetty's `org.eclipse.jetty.util.ajax.JSON` class, and the other based on the https://github.com/FasterXML/jackson[Jackson] library. +Two implementations are available, one based on Jetty's `jetty-util-ajax` library, mainly classes `org.eclipse.jetty.util.ajax.JSON` and `org.eclipse.jetty.util.ajax.AsyncJSON`, and the other based on the https://github.com/FasterXML/jackson[Jackson] library. The default implementation uses the Jetty library. Distinctions between them include: * The Jetty library allows you to plug in custom serializers and deserializers, to fine control the conversion from object to JSON and vice versa, via a custom API. + -Refer to the `org.eclipse.jetty.util.ajax.JSON` https://eclipse.org/jetty/javadoc/current/org/eclipse/jetty/util/ajax/JSON.html[javadocs] for further information. +Refer to the `org.eclipse.jetty.util.ajax.JSON` https://eclipse.org/jetty/javadoc/current/org/eclipse/jetty/util/ajax/JSON.html[javadocs] and `org.eclipse.jetty.util.ajax.AsyncJSON` https://eclipse.org/jetty/javadoc/current/org/eclipse/jetty/util/ajax/AsyncJSON.html[javadocs] for further information. * The Jackson library offers a rich API based on annotations to customize JSON generation, but less so to customize JSON parsing and obtain objects of custom classes. + Jackson does not support out-of-the-box collections created with the collection factory methods introduced in Java 9, such as `Map.of()`, `List.of()`, etc. @@ -133,7 +133,7 @@ include::{doc_code}/JSONDocs.java[tags=portability] Sometimes it is very useful to be able to obtain objects of application classes instead of just `Map` when calling `message.getData()`. You can easily achieve this with the Jetty JSON library. -It is enough that the client formats the JSON object by adding an additional `class` field whose value is the fully qualified class name that you want to convert the JSON to: +It is enough that the client formats the JSON object by adding a `class` field whose value is the fully qualified class name that you want to convert the JSON to: [source,javascript] ---- @@ -151,6 +151,14 @@ On the server, in the `web.xml` file, you register the `org.cometd.server.JettyJ include::{doc_code}/JSONDocs.java[tags=configureConvertor] ---- +Note also that for historical reasons, Jetty's `JSON` and `AsyncJSON` classes deserialize JSON arrays differently, respectively as `Object[]` and as `List`. +However, it is possible to configure both classes with a custom function that performs array conversion, so that JSON arrays can be represented in the same way by both classes: + +[source,java,indent=0] +---- +include::{doc_code}/JSONDocs.java[tags=configureArrayConverter] +---- + Finally, these are the `EchoInfoConvertor` and `EchoInfo` classes: [source,java,indent=0] diff --git a/cometd-documentation/src/main/java/org/cometd/documentation/JSONDocs.java b/cometd-documentation/src/main/java/org/cometd/documentation/JSONDocs.java index 10eb5e8b28..d910a1c97a 100644 --- a/cometd-documentation/src/main/java/org/cometd/documentation/JSONDocs.java +++ b/cometd-documentation/src/main/java/org/cometd/documentation/JSONDocs.java @@ -99,10 +99,26 @@ public static void portability(ServerMessage message) { public void configureConvertor(BayeuxServer bayeuxServer) { // tag::configureConvertor[] JettyJSONContextServer jsonContext = (JettyJSONContextServer)bayeuxServer.getOption(AbstractServerTransport.JSON_CONTEXT_OPTION); + + // Map the EchoInfo class to its JSON.Convertor implementation. jsonContext.putConvertor(EchoInfo.class.getName(), new EchoInfoConvertor()); // end::configureConvertor[] } + public void configureArrayConverter(BayeuxServer bayeuxServer) { + // tag::configureArrayConverter[] + JettyJSONContextServer jsonContext = (JettyJSONContextServer)bayeuxServer.getOption(AbstractServerTransport.JSON_CONTEXT_OPTION); + + // The default array converter for the JSON class is: list -> list.toArray(). + // Here, instead, retain the list as the representation of JSON arrays. + jsonContext.getJSON().setArrayConverter(list -> list); + + // The default converter for the AsyncJSON class is already: list -> list. + // However, for clarity and consistency, configure it in the same way as above. + jsonContext.getAsyncJSONFactory().setArrayConverter(list -> list); + // end::configureArrayConverter[] + } + // tag::convertor[] public class EchoInfoConvertor implements JSON.Convertor { @Override diff --git a/cometd-java/cometd-java-tests/cometd-java-tests-common/src/test/java/org/cometd/tests/JSONContextTest.java b/cometd-java/cometd-java-tests/cometd-java-tests-common/src/test/java/org/cometd/tests/JSONContextTest.java new file mode 100644 index 0000000000..2f27e785ce --- /dev/null +++ b/cometd-java/cometd-java-tests/cometd-java-tests-common/src/test/java/org/cometd/tests/JSONContextTest.java @@ -0,0 +1,84 @@ +package org.cometd.tests; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import org.cometd.bayeux.client.ClientSessionChannel; +import org.cometd.client.BayeuxClient; +import org.cometd.client.transport.ClientTransport; +import org.cometd.common.JettyJSONContextClient; +import org.cometd.server.AbstractServerTransport; +import org.cometd.server.JettyJSONContextServer; +import org.eclipse.jetty.util.ajax.JSONPojoConvertor; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +public class JSONContextTest extends AbstractClientServerTest { + @ParameterizedTest + @MethodSource("transports") + public void testCustomSerialization(Transport transport) throws Exception { + Map serverOptions = serverOptions(transport); + serverOptions.put(AbstractServerTransport.JSON_CONTEXT_OPTION, CustomJSONContextServer.class.getName()); + startServer(transport, serverOptions); + + Map clientOptions = new HashMap<>(); + clientOptions.put(ClientTransport.JSON_CONTEXT_OPTION, new CustomJSONContextClient()); + BayeuxClient client = new BayeuxClient(cometdURL, newClientTransport(transport, clientOptions)); + + List userIds = Arrays.asList(1L, 2L); + CountDownLatch messageLatch = new CountDownLatch(1); + client.handshake(hsReply -> { + ClientSessionChannel channel = client.getChannel("/custom"); + channel.subscribe((c, m) -> { + Users users = (Users)m.getData(); + Assertions.assertEquals(userIds, users.getUserIds()); + messageLatch.countDown(); + }); + channel.publish(new Users(userIds)); + }); + + Assertions.assertTrue(messageLatch.await(5, TimeUnit.SECONDS)); + + disconnectBayeuxClient(client); + } + + public static class Users { + private List userIds; + + public Users() { + } + + public Users(List userIds) { + setUserIds(userIds); + } + + public List getUserIds() { + return userIds; + } + + public void setUserIds(List userIds) { + this.userIds = userIds; + } + } + + // Parsing a JSON array produces a List in all cases. + // AsyncJSON retains the list as the representation of a JSON array. + // JSON converts the parsed list in a Java array, so it must be configured to retain the list. + public static class CustomJSONContextServer extends JettyJSONContextServer { + public CustomJSONContextServer() { + putConvertor(Users.class.getName(), new JSONPojoConvertor(Users.class)); + getJSON().setArrayConverter(list -> list); + } + } + + public static class CustomJSONContextClient extends JettyJSONContextClient { + public CustomJSONContextClient() { + putConvertor(Users.class.getName(), new JSONPojoConvertor(Users.class)); + getJSON().setArrayConverter(list -> list); + } + } +}