Skip to content

Commit

Permalink
Fixes #1065 - Allow configuring JSON array Java representation.
Browse files Browse the repository at this point in the history
Added test case that shows how to use the new APIs introduced in Jetty 9.4.44/10.0.7/11.0.7.
The defaults are kept in order to avoid breaking existing code.
Updated documentation.

Signed-off-by: Simone Bordet <simone.bordet@gmail.com>
  • Loading branch information
sbordet committed Nov 16, 2021
1 parent 543fdf3 commit a01eda8
Show file tree
Hide file tree
Showing 3 changed files with 111 additions and 3 deletions.
14 changes: 11 additions & 3 deletions cometd-documentation/src/main/asciidoc/java_json.adoc
Expand Up @@ -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.
Expand Down Expand Up @@ -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<String, Object>` 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]
----
Expand All @@ -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]
Expand Down
Expand Up @@ -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
Expand Down
@@ -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<String, String> serverOptions = serverOptions(transport);
serverOptions.put(AbstractServerTransport.JSON_CONTEXT_OPTION, CustomJSONContextServer.class.getName());
startServer(transport, serverOptions);

Map<String, Object> clientOptions = new HashMap<>();
clientOptions.put(ClientTransport.JSON_CONTEXT_OPTION, new CustomJSONContextClient());
BayeuxClient client = new BayeuxClient(cometdURL, newClientTransport(transport, clientOptions));

List<Long> 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<Long> userIds;

public Users() {
}

public Users(List<Long> userIds) {
setUserIds(userIds);
}

public List<Long> getUserIds() {
return userIds;
}

public void setUserIds(List<Long> 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);
}
}
}

0 comments on commit a01eda8

Please sign in to comment.