/*
* 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 org.apache.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 org.apache.brooklyn.api.entity.Entity;
import org.apache.brooklyn.api.mgmt.ManagementContext;
import org.apache.brooklyn.core.entity.Attributes;
import org.apache.brooklyn.core.entity.Entities;
import org.apache.brooklyn.core.mgmt.BrooklynTaskTags;
import org.apache.brooklyn.core.test.entity.LocalManagementContextForTests;
import org.apache.brooklyn.core.test.entity.TestApplication;
import org.apache.brooklyn.core.test.entity.TestEntity;
import org.apache.brooklyn.rest.BrooklynRestApiLauncher;
import org.apache.brooklyn.util.collections.MutableList;
import org.apache.brooklyn.util.collections.MutableMap;
import org.apache.brooklyn.util.http.HttpTool;
import org.apache.brooklyn.util.exceptions.Exceptions;
import org.apache.brooklyn.util.stream.Streams;
import org.apache.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;
import org.eclipse.jetty.server.NetworkConnector;
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:"+((NetworkConnector)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.config().set(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:"+((NetworkConnector)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.config().set(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.config().set(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.config().set(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.config().set(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();
}
}