package com.fasterxml.jackson.databind.contextual; import java.io.IOException; import java.lang.annotation.*; import java.util.*; import com.fasterxml.jackson.annotation.*; import com.fasterxml.jackson.core.*; import com.fasterxml.jackson.databind.*; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.fasterxml.jackson.databind.module.SimpleModule; import com.fasterxml.jackson.databind.ser.ContextualSerializer; import com.fasterxml.jackson.databind.ser.ResolvableSerializer; /** * Test cases to verify that it is possible to define serializers * that can use contextual information (like field/method * annotations) for configuration. */ public class TestContextualSerialization extends BaseMapTest { // NOTE: important; MUST be considered a 'Jackson' annotation to be seen // (or recognized otherwise via AnnotationIntrospect.isHandled()) @Target({ElementType.FIELD, ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @JacksonAnnotation public @interface Prefix { public String value(); } static class ContextualBean { protected final String _value; public ContextualBean(String s) { _value = s; } @Prefix("see:") public String getValue() { return _value; } } // For [JACKSON-569] static class AnnotatedContextualBean { @Prefix("prefix->") @JsonSerialize(using=AnnotatedContextualSerializer.class) protected final String value; public AnnotatedContextualBean(String s) { value = s; } } @Prefix("wrappedBean:") static class ContextualBeanWrapper { @Prefix("wrapped:") public ContextualBean wrapped; public ContextualBeanWrapper(String s) { wrapped = new ContextualBean(s); } } static class ContextualArrayBean { @Prefix("array->") public final String[] beans; public ContextualArrayBean(String... strings) { beans = strings; } } static class ContextualArrayElementBean { @Prefix("elem->") @JsonSerialize(contentUsing=AnnotatedContextualSerializer.class) public final String[] beans; public ContextualArrayElementBean(String... strings) { beans = strings; } } static class ContextualListBean { @Prefix("list->") public final List beans = new ArrayList(); public ContextualListBean(String... strings) { for (String string : strings) { beans.add(string); } } } static class ContextualMapBean { @Prefix("map->") public final Map beans = new HashMap(); } /** * Another bean that has class annotations that should be visible for * contextualizer, too */ @Prefix("Voila->") static class BeanWithClassConfig { public String value; public BeanWithClassConfig(String v) { value = v; } } /** * Annotation-based contextual serializer that simply prepends piece of text. */ static class AnnotatedContextualSerializer extends JsonSerializer implements ContextualSerializer { protected final String _prefix; public AnnotatedContextualSerializer() { this(""); } public AnnotatedContextualSerializer(String p) { _prefix = p; } @Override public void serialize(String value, JsonGenerator jgen, SerializerProvider provider) throws IOException { jgen.writeString(_prefix + value); } @Override public JsonSerializer createContextual(SerializerProvider prov, BeanProperty property) throws JsonMappingException { String prefix = "UNKNOWN"; Prefix ann = null; if (property != null) { ann = property.getAnnotation(Prefix.class); if (ann == null) { ann = property.getContextAnnotation(Prefix.class); } } if (ann != null) { prefix = ann.value(); } return new AnnotatedContextualSerializer(prefix); } } static class ContextualAndResolvable extends JsonSerializer implements ContextualSerializer, ResolvableSerializer { protected int isContextual; protected int isResolved; public ContextualAndResolvable() { this(0, 0); } public ContextualAndResolvable(int resolved, int contextual) { isContextual = contextual; isResolved = resolved; } @Override public void serialize(String value, JsonGenerator jgen, SerializerProvider provider) throws IOException { jgen.writeString("contextual="+isContextual+",resolved="+isResolved); } @Override public JsonSerializer createContextual(SerializerProvider prov, BeanProperty property) throws JsonMappingException { return new ContextualAndResolvable(isResolved, isContextual+1); } @Override public void resolve(SerializerProvider provider) { ++isResolved; } } static class AccumulatingContextual extends JsonSerializer implements ContextualSerializer { protected String desc; public AccumulatingContextual() { this(""); } public AccumulatingContextual(String newDesc) { desc = newDesc; } @Override public void serialize(String value, JsonGenerator g, SerializerProvider provider) throws IOException { g.writeString(desc+"/"+value); } @Override public JsonSerializer createContextual(SerializerProvider prov, BeanProperty property) throws JsonMappingException { if (property == null) { return new AccumulatingContextual(desc+"/ROOT"); } return new AccumulatingContextual(desc+"/"+property.getName()); } } /* /********************************************************** /* Unit tests /********************************************************** */ // Test to verify that contextual serializer can make use of property // (method, field) annotations. public void testMethodAnnotations() throws Exception { ObjectMapper mapper = new ObjectMapper(); SimpleModule module = new SimpleModule("test", Version.unknownVersion()); module.addSerializer(String.class, new AnnotatedContextualSerializer()); mapper.registerModule(module); assertEquals("{\"value\":\"see:foobar\"}", mapper.writeValueAsString(new ContextualBean("foobar"))); } // Test to verify that contextual serializer can also use annotations // for enclosing class. public void testClassAnnotations() throws Exception { ObjectMapper mapper = new ObjectMapper(); SimpleModule module = new SimpleModule("test", Version.unknownVersion()); module.addSerializer(String.class, new AnnotatedContextualSerializer()); mapper.registerModule(module); assertEquals("{\"value\":\"Voila->xyz\"}", mapper.writeValueAsString(new BeanWithClassConfig("xyz"))); } public void testWrappedBean() throws Exception { ObjectMapper mapper = new ObjectMapper(); SimpleModule module = new SimpleModule("test", Version.unknownVersion()); module.addSerializer(String.class, new AnnotatedContextualSerializer()); mapper.registerModule(module); assertEquals("{\"wrapped\":{\"value\":\"see:xyz\"}}", mapper.writeValueAsString(new ContextualBeanWrapper("xyz"))); } // Serializer should get passed property context even if contained in an array. public void testMethodAnnotationInArray() throws Exception { ObjectMapper mapper = new ObjectMapper(); SimpleModule module = new SimpleModule("test", Version.unknownVersion()); module.addSerializer(String.class, new AnnotatedContextualSerializer()); mapper.registerModule(module); ContextualArrayBean beans = new ContextualArrayBean("123"); assertEquals("{\"beans\":[\"array->123\"]}", mapper.writeValueAsString(beans)); } // Serializer should get passed property context even if contained in a Collection. public void testMethodAnnotationInList() throws Exception { ObjectMapper mapper = new ObjectMapper(); SimpleModule module = new SimpleModule("test", Version.unknownVersion()); module.addSerializer(String.class, new AnnotatedContextualSerializer()); mapper.registerModule(module); ContextualListBean beans = new ContextualListBean("abc"); assertEquals("{\"beans\":[\"list->abc\"]}", mapper.writeValueAsString(beans)); } // Serializer should get passed property context even if contained in a Collection. public void testMethodAnnotationInMap() throws Exception { ObjectMapper mapper = new ObjectMapper(); SimpleModule module = new SimpleModule("test", Version.unknownVersion()); module.addSerializer(String.class, new AnnotatedContextualSerializer()); mapper.registerModule(module); ContextualMapBean map = new ContextualMapBean(); map.beans.put("first", "In Map"); assertEquals("{\"beans\":{\"first\":\"map->In Map\"}}", mapper.writeValueAsString(map)); } public void testContextualViaAnnotation() throws Exception { ObjectMapper mapper = new ObjectMapper(); AnnotatedContextualBean bean = new AnnotatedContextualBean("abc"); assertEquals("{\"value\":\"prefix->abc\"}", mapper.writeValueAsString(bean)); } public void testResolveOnContextual() throws Exception { SimpleModule module = new SimpleModule("test", Version.unknownVersion()); module.addSerializer(String.class, new ContextualAndResolvable()); ObjectMapper mapper = jsonMapperBuilder() .addModule(module) .build(); assertEquals(quote("contextual=1,resolved=1"), mapper.writeValueAsString("abc")); // also: should NOT be called again assertEquals(quote("contextual=1,resolved=1"), mapper.writeValueAsString("foo")); } public void testContextualArrayElement() throws Exception { ObjectMapper mapper = newJsonMapper(); ContextualArrayElementBean beans = new ContextualArrayElementBean("456"); assertEquals("{\"beans\":[\"elem->456\"]}", mapper.writeValueAsString(beans)); } // Test to verify aspects of [databind#2429] public void testRootContextualization2429() throws Exception { ObjectMapper mapper = jsonMapperBuilder() .addModule(new SimpleModule("test", Version.unknownVersion()) .addSerializer(String.class, new AccumulatingContextual())) .build(); assertEquals(quote("/ROOT/foo"), mapper.writeValueAsString("foo")); assertEquals(quote("/ROOT/bar"), mapper.writeValueAsString("bar")); assertEquals(quote("/ROOT/3"), mapper.writeValueAsString("3")); } }