/* * Copyright (c) 2008-2017 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.cometd.client; import java.io.StringReader; import java.util.HashMap; import java.util.Map; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import com.fasterxml.jackson.databind.ObjectMapper; import org.cometd.bayeux.Message; import org.cometd.bayeux.client.ClientSession; import org.cometd.bayeux.client.ClientSessionChannel; import org.cometd.bayeux.server.LocalSession; import org.cometd.client.transport.ClientTransport; import org.cometd.client.transport.LongPollingTransport; import org.cometd.common.JSONContext; import org.cometd.common.JacksonJSONContextClient; import org.cometd.server.AbstractServerTransport; import org.cometd.server.JacksonJSONContextServer; import org.cometd.server.transport.AbstractHttpTransport; import org.junit.Assert; import org.junit.Test; public class JacksonCustomSerializationTest extends ClientServerTest { @Test public void testJacksonCustomSerialization() throws Exception { Map<String, String> serverOptions = new HashMap<>(); serverOptions.put(AbstractServerTransport.JSON_CONTEXT_OPTION, TestJacksonJSONContextServer.class.getName()); serverOptions.put(AbstractHttpTransport.JSON_DEBUG_OPTION, "true"); Map<String, Object> clientOptions = new HashMap<>(); clientOptions.put(ClientTransport.JSON_CONTEXT_OPTION, TestJacksonJSONContextClient.class.getName()); startServer(serverOptions); String channelName = "/data"; final String dataContent = "random"; final long extraContent = 13; final CountDownLatch latch = new CountDownLatch(1); LocalSession service = bayeux.newLocalSession("custom_serialization"); service.handshake(); service.getChannel(channelName).subscribe(new ClientSessionChannel.MessageListener() { @Override public void onMessage(ClientSessionChannel channel, Message message) { Data data = (Data)message.getData(); Assert.assertEquals(dataContent, data.content); Map<String, Object> ext = message.getExt(); Assert.assertNotNull(ext); Extra extra = (Extra)ext.get("extra"); Assert.assertEquals(extraContent, extra.content); latch.countDown(); } }); BayeuxClient client = new BayeuxClient(cometdURL, new LongPollingTransport(clientOptions, httpClient)); client.addExtension(new ExtraExtension(extraContent)); client.handshake(); Assert.assertTrue(client.waitFor(5000, BayeuxClient.State.CONNECTED)); // Wait for the connect to establish Thread.sleep(1000); client.getChannel(channelName).publish(new Data(dataContent)); Assert.assertTrue(latch.await(5, TimeUnit.SECONDS)); disconnectBayeuxClient(client); } @Test public void testParserGenerator() throws Exception { // Note: Jackson does not seem to be able to serialize/deserialize correctly a single Data/Extra object. // However, if they are put into a container like a Map, then Jackson produces a different JSON than // what it produces for the standalone object that allows correct deserialization, of this form: // { field: ["className", {object}] } // It is way easier to have Jetty serialize and deserialize this form than make Jackson use Jetty's form. // They problem is that Jackson tries to be "smart" in figuring out the typing, but with a Map<String, Object> // there is no way to have type information for the values, so Jackson defaults to a basic deserializer // that either is not very flexible, or it's very difficult to configure, so much that I could not so far. JSONContext.Client jsonContext = new TestJacksonJSONContextClient(); Data data1 = new Data("data"); Extra extra1 = new Extra(42L); Map<String, Object> map1 = new HashMap<>(); map1.put("data", data1); map1.put("extra", extra1); String json = jsonContext.getGenerator().generate(map1); Map map2 = jsonContext.getParser().parse(new StringReader(json), Map.class); Data data2 = (Data)map2.get("data"); Extra extra2 = (Extra)map2.get("extra"); Assert.assertEquals(data1.content, data2.content); Assert.assertEquals(extra1.content, extra2.content); } private static class ExtraExtension extends ClientSession.Extension.Adapter { private final long content; public ExtraExtension(long content) { this.content = content; } @Override public boolean send(ClientSession session, Message.Mutable message) { Map<String, Object> ext = message.getExt(true); ext.put("extra", new Extra(content)); return true; } } public static class TestJacksonJSONContextServer extends JacksonJSONContextServer { public TestJacksonJSONContextServer() { getObjectMapper().enableDefaultTyping(ObjectMapper.DefaultTyping.JAVA_LANG_OBJECT); } } public static class TestJacksonJSONContextClient extends JacksonJSONContextClient { public TestJacksonJSONContextClient() { getObjectMapper().enableDefaultTyping(ObjectMapper.DefaultTyping.JAVA_LANG_OBJECT); } } private static class Data { @com.fasterxml.jackson.annotation.JsonProperty private String content; private Data() { // Needed by Jackson } private Data(String content) { this.content = content; } } private static class Extra { @com.fasterxml.jackson.annotation.JsonProperty private long content; private Extra() { // Needed by Jackson } private Extra(long content) { this.content = content; } } }