1 package com.fasterxml.jackson.databind.contextual; 2 3 import java.io.IOException; 4 import java.lang.annotation.*; 5 import java.util.*; 6 7 import com.fasterxml.jackson.annotation.*; 8 9 import com.fasterxml.jackson.core.*; 10 import com.fasterxml.jackson.databind.*; 11 import com.fasterxml.jackson.databind.annotation.JsonSerialize; 12 import com.fasterxml.jackson.databind.module.SimpleModule; 13 import com.fasterxml.jackson.databind.ser.ContextualSerializer; 14 import com.fasterxml.jackson.databind.ser.ResolvableSerializer; 15 16 /** 17 * Test cases to verify that it is possible to define serializers 18 * that can use contextual information (like field/method 19 * annotations) for configuration. 20 */ 21 public class TestContextualSerialization extends BaseMapTest 22 { 23 // NOTE: important; MUST be considered a 'Jackson' annotation to be seen 24 // (or recognized otherwise via AnnotationIntrospect.isHandled()) 25 @Target({ElementType.FIELD, ElementType.TYPE, ElementType.METHOD}) 26 @Retention(RetentionPolicy.RUNTIME) 27 @JacksonAnnotation 28 public @interface Prefix { value()29 public String value(); 30 } 31 32 static class ContextualBean 33 { 34 protected final String _value; 35 ContextualBean(String s)36 public ContextualBean(String s) { _value = s; } 37 38 @Prefix("see:") getValue()39 public String getValue() { return _value; } 40 } 41 42 // For [JACKSON-569] 43 static class AnnotatedContextualBean 44 { 45 @Prefix("prefix->") 46 @JsonSerialize(using=AnnotatedContextualSerializer.class) 47 protected final String value; 48 AnnotatedContextualBean(String s)49 public AnnotatedContextualBean(String s) { value = s; } 50 } 51 52 53 @Prefix("wrappedBean:") 54 static class ContextualBeanWrapper 55 { 56 @Prefix("wrapped:") 57 public ContextualBean wrapped; 58 ContextualBeanWrapper(String s)59 public ContextualBeanWrapper(String s) { 60 wrapped = new ContextualBean(s); 61 } 62 } 63 64 static class ContextualArrayBean 65 { 66 @Prefix("array->") 67 public final String[] beans; 68 ContextualArrayBean(String... strings)69 public ContextualArrayBean(String... strings) { 70 beans = strings; 71 } 72 } 73 74 static class ContextualArrayElementBean 75 { 76 @Prefix("elem->") 77 @JsonSerialize(contentUsing=AnnotatedContextualSerializer.class) 78 public final String[] beans; 79 ContextualArrayElementBean(String... strings)80 public ContextualArrayElementBean(String... strings) { 81 beans = strings; 82 } 83 } 84 85 static class ContextualListBean 86 { 87 @Prefix("list->") 88 public final List<String> beans = new ArrayList<String>(); 89 ContextualListBean(String... strings)90 public ContextualListBean(String... strings) { 91 for (String string : strings) { 92 beans.add(string); 93 } 94 } 95 } 96 97 static class ContextualMapBean 98 { 99 @Prefix("map->") 100 public final Map<String, String> beans = new HashMap<String, String>(); 101 } 102 103 /** 104 * Another bean that has class annotations that should be visible for 105 * contextualizer, too 106 */ 107 @Prefix("Voila->") 108 static class BeanWithClassConfig 109 { 110 public String value; 111 BeanWithClassConfig(String v)112 public BeanWithClassConfig(String v) { value = v; } 113 } 114 115 /** 116 * Annotation-based contextual serializer that simply prepends piece of text. 117 */ 118 static class AnnotatedContextualSerializer 119 extends JsonSerializer<String> 120 implements ContextualSerializer 121 { 122 protected final String _prefix; 123 AnnotatedContextualSerializer()124 public AnnotatedContextualSerializer() { this(""); } AnnotatedContextualSerializer(String p)125 public AnnotatedContextualSerializer(String p) { 126 _prefix = p; 127 } 128 129 @Override serialize(String value, JsonGenerator jgen, SerializerProvider provider)130 public void serialize(String value, JsonGenerator jgen, SerializerProvider provider) throws IOException 131 { 132 jgen.writeString(_prefix + value); 133 } 134 135 @Override createContextual(SerializerProvider prov, BeanProperty property)136 public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property) 137 throws JsonMappingException 138 { 139 String prefix = "UNKNOWN"; 140 Prefix ann = null; 141 if (property != null) { 142 ann = property.getAnnotation(Prefix.class); 143 if (ann == null) { 144 ann = property.getContextAnnotation(Prefix.class); 145 } 146 } 147 if (ann != null) { 148 prefix = ann.value(); 149 } 150 return new AnnotatedContextualSerializer(prefix); 151 } 152 } 153 154 static class ContextualAndResolvable 155 extends JsonSerializer<String> 156 implements ContextualSerializer, ResolvableSerializer 157 { 158 protected int isContextual; 159 protected int isResolved; 160 ContextualAndResolvable()161 public ContextualAndResolvable() { this(0, 0); } 162 ContextualAndResolvable(int resolved, int contextual)163 public ContextualAndResolvable(int resolved, int contextual) 164 { 165 isContextual = contextual; 166 isResolved = resolved; 167 } 168 169 @Override serialize(String value, JsonGenerator jgen, SerializerProvider provider)170 public void serialize(String value, JsonGenerator jgen, SerializerProvider provider) throws IOException 171 { 172 jgen.writeString("contextual="+isContextual+",resolved="+isResolved); 173 } 174 175 @Override createContextual(SerializerProvider prov, BeanProperty property)176 public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property) 177 throws JsonMappingException 178 { 179 return new ContextualAndResolvable(isResolved, isContextual+1); 180 } 181 182 @Override resolve(SerializerProvider provider)183 public void resolve(SerializerProvider provider) { 184 ++isResolved; 185 } 186 } 187 188 static class AccumulatingContextual 189 extends JsonSerializer<String> 190 implements ContextualSerializer 191 { 192 protected String desc; 193 AccumulatingContextual()194 public AccumulatingContextual() { this(""); } 195 AccumulatingContextual(String newDesc)196 public AccumulatingContextual(String newDesc) { 197 desc = newDesc; 198 } 199 200 @Override serialize(String value, JsonGenerator g, SerializerProvider provider)201 public void serialize(String value, JsonGenerator g, SerializerProvider provider) throws IOException 202 { 203 g.writeString(desc+"/"+value); 204 } 205 206 @Override createContextual(SerializerProvider prov, BeanProperty property)207 public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property) 208 throws JsonMappingException 209 { 210 if (property == null) { 211 return new AccumulatingContextual(desc+"/ROOT"); 212 } 213 return new AccumulatingContextual(desc+"/"+property.getName()); 214 } 215 } 216 217 /* 218 /********************************************************** 219 /* Unit tests 220 /********************************************************** 221 */ 222 223 // Test to verify that contextual serializer can make use of property 224 // (method, field) annotations. testMethodAnnotations()225 public void testMethodAnnotations() throws Exception 226 { 227 ObjectMapper mapper = new ObjectMapper(); 228 SimpleModule module = new SimpleModule("test", Version.unknownVersion()); 229 module.addSerializer(String.class, new AnnotatedContextualSerializer()); 230 mapper.registerModule(module); 231 assertEquals("{\"value\":\"see:foobar\"}", mapper.writeValueAsString(new ContextualBean("foobar"))); 232 } 233 234 // Test to verify that contextual serializer can also use annotations 235 // for enclosing class. testClassAnnotations()236 public void testClassAnnotations() throws Exception 237 { 238 ObjectMapper mapper = new ObjectMapper(); 239 SimpleModule module = new SimpleModule("test", Version.unknownVersion()); 240 module.addSerializer(String.class, new AnnotatedContextualSerializer()); 241 mapper.registerModule(module); 242 assertEquals("{\"value\":\"Voila->xyz\"}", mapper.writeValueAsString(new BeanWithClassConfig("xyz"))); 243 } 244 testWrappedBean()245 public void testWrappedBean() throws Exception 246 { 247 ObjectMapper mapper = new ObjectMapper(); 248 SimpleModule module = new SimpleModule("test", Version.unknownVersion()); 249 module.addSerializer(String.class, new AnnotatedContextualSerializer()); 250 mapper.registerModule(module); 251 assertEquals("{\"wrapped\":{\"value\":\"see:xyz\"}}", mapper.writeValueAsString(new ContextualBeanWrapper("xyz"))); 252 } 253 254 // Serializer should get passed property context even if contained in an array. testMethodAnnotationInArray()255 public void testMethodAnnotationInArray() throws Exception 256 { 257 ObjectMapper mapper = new ObjectMapper(); 258 SimpleModule module = new SimpleModule("test", Version.unknownVersion()); 259 module.addSerializer(String.class, new AnnotatedContextualSerializer()); 260 mapper.registerModule(module); 261 ContextualArrayBean beans = new ContextualArrayBean("123"); 262 assertEquals("{\"beans\":[\"array->123\"]}", mapper.writeValueAsString(beans)); 263 } 264 265 // Serializer should get passed property context even if contained in a Collection. testMethodAnnotationInList()266 public void testMethodAnnotationInList() throws Exception 267 { 268 ObjectMapper mapper = new ObjectMapper(); 269 SimpleModule module = new SimpleModule("test", Version.unknownVersion()); 270 module.addSerializer(String.class, new AnnotatedContextualSerializer()); 271 mapper.registerModule(module); 272 ContextualListBean beans = new ContextualListBean("abc"); 273 assertEquals("{\"beans\":[\"list->abc\"]}", mapper.writeValueAsString(beans)); 274 } 275 276 // Serializer should get passed property context even if contained in a Collection. testMethodAnnotationInMap()277 public void testMethodAnnotationInMap() throws Exception 278 { 279 ObjectMapper mapper = new ObjectMapper(); 280 SimpleModule module = new SimpleModule("test", Version.unknownVersion()); 281 module.addSerializer(String.class, new AnnotatedContextualSerializer()); 282 mapper.registerModule(module); 283 ContextualMapBean map = new ContextualMapBean(); 284 map.beans.put("first", "In Map"); 285 assertEquals("{\"beans\":{\"first\":\"map->In Map\"}}", mapper.writeValueAsString(map)); 286 } 287 testContextualViaAnnotation()288 public void testContextualViaAnnotation() throws Exception 289 { 290 ObjectMapper mapper = new ObjectMapper(); 291 AnnotatedContextualBean bean = new AnnotatedContextualBean("abc"); 292 assertEquals("{\"value\":\"prefix->abc\"}", mapper.writeValueAsString(bean)); 293 } 294 testResolveOnContextual()295 public void testResolveOnContextual() throws Exception 296 { 297 SimpleModule module = new SimpleModule("test", Version.unknownVersion()); 298 module.addSerializer(String.class, new ContextualAndResolvable()); 299 ObjectMapper mapper = jsonMapperBuilder() 300 .addModule(module) 301 .build(); 302 assertEquals(quote("contextual=1,resolved=1"), mapper.writeValueAsString("abc")); 303 304 // also: should NOT be called again 305 assertEquals(quote("contextual=1,resolved=1"), mapper.writeValueAsString("foo")); 306 } 307 testContextualArrayElement()308 public void testContextualArrayElement() throws Exception 309 { 310 ObjectMapper mapper = newJsonMapper(); 311 ContextualArrayElementBean beans = new ContextualArrayElementBean("456"); 312 assertEquals("{\"beans\":[\"elem->456\"]}", mapper.writeValueAsString(beans)); 313 } 314 315 // Test to verify aspects of [databind#2429] testRootContextualization2429()316 public void testRootContextualization2429() throws Exception 317 { 318 ObjectMapper mapper = jsonMapperBuilder() 319 .addModule(new SimpleModule("test", Version.unknownVersion()) 320 .addSerializer(String.class, new AccumulatingContextual())) 321 .build(); 322 assertEquals(quote("/ROOT/foo"), mapper.writeValueAsString("foo")); 323 assertEquals(quote("/ROOT/bar"), mapper.writeValueAsString("bar")); 324 assertEquals(quote("/ROOT/3"), mapper.writeValueAsString("3")); 325 } 326 } 327