brooklyn.rest.util.json.BrooklynJacksonSerializerTest.java Source code

Java tutorial

Introduction

Here is the source code for brooklyn.rest.util.json.BrooklynJacksonSerializerTest.java

Source

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 brooklyn.rest.util.json;

import java.io.NotSerializableException;
import java.net.URI;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import javax.ws.rs.core.MediaType;

import org.apache.http.client.HttpClient;
import org.apache.http.client.utils.URIBuilder;
import org.codehaus.jackson.annotate.JsonProperty;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.map.annotate.JsonSerialize;
import org.eclipse.jetty.server.Server;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.testng.Assert;
import org.testng.annotations.Test;

import brooklyn.entity.Entity;
import brooklyn.entity.basic.Attributes;
import brooklyn.entity.basic.BrooklynTaskTags;
import brooklyn.entity.basic.Entities;
import brooklyn.management.ManagementContext;
import brooklyn.rest.BrooklynRestApiLauncher;
import brooklyn.test.entity.LocalManagementContextForTests;
import brooklyn.test.entity.TestApplication;
import brooklyn.test.entity.TestEntity;
import brooklyn.util.collections.MutableList;
import brooklyn.util.collections.MutableMap;
import brooklyn.util.exceptions.Exceptions;
import brooklyn.util.http.HttpTool;
import brooklyn.util.stream.Streams;
import brooklyn.util.text.Strings;

import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Multimap;
import com.google.common.collect.MultimapBuilder;
import com.google.gson.Gson;

public class BrooklynJacksonSerializerTest {

    private static final Logger log = LoggerFactory.getLogger(BrooklynJacksonSerializerTest.class);

    public static class SillyClassWithManagementContext {
        @JsonProperty
        ManagementContext mgmt;
        @JsonProperty
        String id;

        public SillyClassWithManagementContext() {
        }

        public SillyClassWithManagementContext(String id, ManagementContext mgmt) {
            this.id = id;
            this.mgmt = mgmt;
        }

        @Override
        public String toString() {
            return super.toString() + "[id=" + id + ";mgmt=" + mgmt + "]";
        }
    }

    @Test
    public void testCustomSerializerWithSerializableSillyManagementExample() throws Exception {
        ManagementContext mgmt = LocalManagementContextForTests.newInstance();
        try {

            ObjectMapper mapper = BrooklynJacksonJsonProvider.newPrivateObjectMapper(mgmt);

            SillyClassWithManagementContext silly = new SillyClassWithManagementContext("123", mgmt);
            log.info("silly is: " + silly);

            String sillyS = mapper.writeValueAsString(silly);

            log.info("silly json is: " + sillyS);

            SillyClassWithManagementContext silly2 = mapper.readValue(sillyS,
                    SillyClassWithManagementContext.class);
            log.info("silly2 is: " + silly2);

            Assert.assertEquals(silly.id, silly2.id);

        } finally {
            Entities.destroyAll(mgmt);
        }
    }

    public static class SelfRefNonSerializableClass {
        @JsonProperty
        Object bogus = this;
    }

    @Test
    public void testSelfReferenceFailsWhenStrict() {
        checkNonSerializableWhenStrict(new SelfRefNonSerializableClass());
    }

    @Test
    public void testSelfReferenceGeneratesErrorMapObject() throws Exception {
        checkSerializesAsMapWithErrorAndToString(new SelfRefNonSerializableClass());
    }

    @Test
    public void testNonSerializableInListIsShownInList() throws Exception {
        List<?> result = checkSerializesAs(MutableList.of(1, new SelfRefNonSerializableClass()), List.class);
        Assert.assertEquals(result.get(0), 1);
        Assert.assertEquals(((Map<?, ?>) result.get(1)).get("errorType"), NotSerializableException.class.getName());
    }

    @Test
    public void testNonSerializableInMapIsShownInMap() throws Exception {
        Map<?, ?> result = checkSerializesAs(MutableMap.of("x", new SelfRefNonSerializableClass()), Map.class);
        Assert.assertEquals(((Map<?, ?>) result.get("x")).get("errorType"),
                NotSerializableException.class.getName());
    }

    static class TupleWithNonSerializable {
        String good = "bon";
        SelfRefNonSerializableClass bad = new SelfRefNonSerializableClass();
    }

    @Test
    public void testNonSerializableInObjectIsShownInMap() throws Exception {
        String resultS = checkSerializesAs(new TupleWithNonSerializable(), null);
        log.info("nested non-serializable json is " + resultS);
        Assert.assertTrue(resultS.startsWith("{\"good\":\"bon\",\"bad\":{"),
                "expected a nested map for the error field, not " + resultS);

        Map<?, ?> result = checkSerializesAs(new TupleWithNonSerializable(), Map.class);
        Assert.assertEquals(result.get("good"), "bon");
        Assert.assertTrue(result.containsKey("bad"), "Should have had a key for field 'bad'");
        Assert.assertEquals(((Map<?, ?>) result.get("bad")).get("errorType"),
                NotSerializableException.class.getName());
    }

    public static class EmptyClass {
    }

    @Test
    public void testEmptySerializesAsEmpty() throws Exception {
        // deliberately, a class with no fields and no annotations serializes as an error,
        // because the alternative, {}, is useless.  however if it *is* annotated, as below, then it will serialize fine.
        checkSerializesAsMapWithErrorAndToString(new SelfRefNonSerializableClass());
    }

    @Test
    public void testEmptyNonSerializableFailsWhenStrict() {
        checkNonSerializableWhenStrict(new EmptyClass());
    }

    @JsonSerialize
    public static class EmptyClassWithSerialize {
    }

    @Test
    public void testEmptyAnnotatedSerializesAsEmptyEvenWhenStrict() throws Exception {
        try {
            BidiSerialization.setStrictSerialization(true);
            testEmptyAnnotatedSerializesAsEmpty();
        } finally {
            BidiSerialization.clearStrictSerialization();
        }
    }

    @Test
    public void testEmptyAnnotatedSerializesAsEmpty() throws Exception {
        Map<?, ?> map = checkSerializesAs(new EmptyClassWithSerialize(), Map.class);
        Assert.assertTrue(map.isEmpty(), "Expected an empty map; instead got: " + map);

        String result = checkSerializesAs(MutableList.of(new EmptyClassWithSerialize()), null);
        result = result.replaceAll(" ", "").trim();
        Assert.assertEquals(result, "[{}]");
    }

    @Test
    public void testSensorFailsWhenStrict() {
        checkNonSerializableWhenStrict(MutableList.of(Attributes.HTTP_PORT));
    }

    @Test
    public void testSensorSensible() throws Exception {
        Map<?, ?> result = checkSerializesAs(Attributes.HTTP_PORT, Map.class);
        log.info("SENSOR json is: " + result);
        Assert.assertFalse(result.toString().contains("error"),
                "Shouldn't have had an error, instead got: " + result);
    }

    @Test
    public void testLinkedListSerialization() throws Exception {
        LinkedList<Object> ll = new LinkedList<Object>();
        ll.add(1);
        ll.add("two");
        String result = checkSerializesAs(ll, null);
        log.info("LLIST json is: " + result);
        Assert.assertFalse(result.contains("error"), "Shouldn't have had an error, instead got: " + result);
        Assert.assertEquals(Strings.collapseWhitespace(result, ""), "[1,\"two\"]");
    }

    @Test
    public void testMultiMapSerialization() throws Exception {
        Multimap<String, Integer> m = MultimapBuilder.hashKeys().arrayListValues().build();
        m.put("bob", 24);
        m.put("bob", 25);
        String result = checkSerializesAs(m, null);
        log.info("multimap serialized as: " + result);
        Assert.assertFalse(result.contains("error"), "Shouldn't have had an error, instead got: " + result);
        Assert.assertEquals(Strings.collapseWhitespace(result, ""), "{\"bob\":[24,25]}");
    }

    @Test
    public void testSupplierSerialization() throws Exception {
        String result = checkSerializesAs(Strings.toStringSupplier(Streams.byteArrayOfString("x")), null);
        log.info("SUPPLIER json is: " + result);
        Assert.assertFalse(result.contains("error"), "Shouldn't have had an error, instead got: " + result);
    }

    @Test
    public void testWrappedStreamSerialization() throws Exception {
        String result = checkSerializesAs(BrooklynTaskTags.tagForStream("TEST", Streams.byteArrayOfString("x")),
                null);
        log.info("WRAPPED STREAM json is: " + result);
        Assert.assertFalse(result.contains("error"), "Shouldn't have had an error, instead got: " + result);
    }

    @SuppressWarnings("unchecked")
    protected <T> T checkSerializesAs(Object x, Class<T> type) {
        ManagementContext mgmt = LocalManagementContextForTests.newInstance();
        try {
            ObjectMapper mapper = BrooklynJacksonJsonProvider.newPrivateObjectMapper(mgmt);
            String tS = mapper.writeValueAsString(x);
            log.debug("serialized " + x + " as " + tS);
            Assert.assertTrue(tS.length() < 1000, "Data too long, size " + tS.length() + " for " + x);
            if (type == null)
                return (T) tS;
            return mapper.readValue(tS, type);
        } catch (Exception e) {
            throw Exceptions.propagate(e);
        } finally {
            Entities.destroyAll(mgmt);
        }
    }

    protected Map<?, ?> checkSerializesAsMapWithErrorAndToString(Object x) {
        Map<?, ?> rt = checkSerializesAs(x, Map.class);
        Assert.assertEquals(rt.get("toString"), x.toString());
        Assert.assertEquals(rt.get("error"), Boolean.TRUE);
        return rt;
    }

    protected void checkNonSerializableWhenStrict(Object x) {
        checkNonSerializable(x, true);
    }

    protected void checkNonSerializable(Object x, boolean strict) {
        ManagementContext mgmt = LocalManagementContextForTests.newInstance();
        try {
            ObjectMapper mapper = BrooklynJacksonJsonProvider.newPrivateObjectMapper(mgmt);
            if (strict)
                BidiSerialization.setStrictSerialization(true);

            String tS = mapper.writeValueAsString(x);
            Assert.fail("Should not have serialized " + x + "; instead gave: " + tS);

        } catch (Exception e) {
            Exceptions.propagateIfFatal(e);
            log.info("Got expected error, when serializing " + x + ": " + e);

        } finally {
            if (strict)
                BidiSerialization.clearStrictSerialization();
            Entities.destroyAll(mgmt);
        }
    }

    // Ensure TEXT_PLAIN just returns toString for ManagementContext instance.
    // Strangely, testWithLauncherSerializingListsContainingEntitiesAndOtherComplexStuff ended up in the 
    // EntityConfigResource.getPlain code, throwing a ClassCastException.
    // 
    // TODO This tests the fix for that ClassCastException, but does not explain why 
    // testWithLauncherSerializingListsContainingEntitiesAndOtherComplexStuff was calling it.
    @Test(groups = "Integration") //because of time
    public void testWithAcceptsPlainText() throws Exception {
        ManagementContext mgmt = LocalManagementContextForTests.newInstance();
        Server server = null;
        try {
            server = BrooklynRestApiLauncher.launcher().managementContext(mgmt).start();
            HttpClient client = HttpTool.httpClientBuilder().build();

            TestApplication app = TestApplication.Factory.newManagedInstanceForTests(mgmt);

            String serverAddress = "http://localhost:" + server.getConnectors()[0].getLocalPort();
            String appUrl = serverAddress + "/v1/applications/" + app.getId();
            String entityUrl = appUrl + "/entities/" + app.getId();
            URI configUri = new URIBuilder(entityUrl + "/config/" + TestEntity.CONF_OBJECT.getName())
                    .addParameter("raw", "true").build();

            // assert config here is just mgmt.toString()
            app.setConfig(TestEntity.CONF_OBJECT, mgmt);
            String content = get(client, configUri, ImmutableMap.of("Accept", MediaType.TEXT_PLAIN));
            log.info("CONFIG MGMT is:\n" + content);
            Assert.assertEquals(content, mgmt.toString(), "content=" + content);

        } finally {
            try {
                if (server != null)
                    server.stop();
            } catch (Exception e) {
                log.warn("failed to stop server: " + e);
            }
            Entities.destroyAll(mgmt);
        }
    }

    @Test(groups = "Integration") //because of time
    public void testWithLauncherSerializingListsContainingEntitiesAndOtherComplexStuff() throws Exception {
        ManagementContext mgmt = LocalManagementContextForTests.newInstance();
        Server server = null;
        try {
            server = BrooklynRestApiLauncher.launcher().managementContext(mgmt).start();
            HttpClient client = HttpTool.httpClientBuilder().build();

            TestApplication app = TestApplication.Factory.newManagedInstanceForTests(mgmt);

            String serverAddress = "http://localhost:" + server.getConnectors()[0].getLocalPort();
            String appUrl = serverAddress + "/v1/applications/" + app.getId();
            String entityUrl = appUrl + "/entities/" + app.getId();
            URI configUri = new URIBuilder(entityUrl + "/config/" + TestEntity.CONF_OBJECT.getName())
                    .addParameter("raw", "true").build();

            // assert config here is just mgmt
            app.setConfig(TestEntity.CONF_OBJECT, mgmt);
            String content = get(client, configUri, ImmutableMap.of("Accept", MediaType.APPLICATION_JSON));
            log.info("CONFIG MGMT is:\n" + content);
            @SuppressWarnings("rawtypes")
            Map values = new Gson().fromJson(content, Map.class);
            Assert.assertEquals(values,
                    ImmutableMap.of("type", LocalManagementContextForTests.class.getCanonicalName()),
                    "values=" + values);

            // assert normal API returns the same, containing links
            content = get(client, entityUrl, ImmutableMap.of("Accept", MediaType.APPLICATION_JSON));
            log.info("ENTITY is: \n" + content);
            values = new Gson().fromJson(content, Map.class);
            Assert.assertTrue(values.size() >= 3, "Map is too small: " + values);
            Assert.assertTrue(values.size() <= 6, "Map is too big: " + values);
            Assert.assertEquals(values.get("type"), TestApplication.class.getCanonicalName(), "values=" + values);
            Assert.assertNotNull(values.get("links"), "Map should have contained links: values=" + values);

            // but config etc returns our nicely json serialized
            app.setConfig(TestEntity.CONF_OBJECT, app);
            content = get(client, configUri, ImmutableMap.of("Accept", MediaType.APPLICATION_JSON));
            log.info("CONFIG ENTITY is:\n" + content);
            values = new Gson().fromJson(content, Map.class);
            Assert.assertEquals(values, ImmutableMap.of("type", Entity.class.getCanonicalName(), "id", app.getId()),
                    "values=" + values);

            // and self-ref gives error + toString
            SelfRefNonSerializableClass angry = new SelfRefNonSerializableClass();
            app.setConfig(TestEntity.CONF_OBJECT, angry);
            content = get(client, configUri, ImmutableMap.of("Accept", MediaType.APPLICATION_JSON));
            log.info("CONFIG ANGRY is:\n" + content);
            assertErrorObjectMatchingToString(content, angry);

            // as does Server
            app.setConfig(TestEntity.CONF_OBJECT, server);
            content = get(client, configUri, ImmutableMap.of("Accept", MediaType.APPLICATION_JSON));
            // NOTE, if using the default visibility / object mapper, the getters of the object are invoked
            // resulting in an object which is huge, 7+MB -- and it wreaks havoc w eclipse console regex parsing!
            // (but with our custom VisibilityChecker server just gives us the nicer error!)
            log.info("CONFIG SERVER is:\n" + content);
            assertErrorObjectMatchingToString(content, server);
            Assert.assertTrue(content.contains(NotSerializableException.class.getCanonicalName()),
                    "server should have contained things which are not serializable");
            Assert.assertTrue(content.length() < 1024,
                    "content should not have been very long; instead was: " + content.length());

        } finally {
            try {
                if (server != null)
                    server.stop();
            } catch (Exception e) {
                log.warn("failed to stop server: " + e);
            }
            Entities.destroyAll(mgmt);
        }
    }

    private void assertErrorObjectMatchingToString(String content, Object expected) {
        Object value = new Gson().fromJson(content, Object.class);
        Assert.assertTrue(value instanceof Map, "Expected map, got: " + value);
        Assert.assertEquals(((Map<?, ?>) value).get("toString"), expected.toString());
    }

    private String get(HttpClient client, String uri, Map<String, String> headers) {
        return get(client, URI.create(uri), headers);
    }

    private String get(HttpClient client, URI uri, Map<String, String> headers) {
        return HttpTool.httpGet(client, uri, headers).getContentAsString();
    }
}