1 package com.fasterxml.jackson.databind.ser; 2 3 import java.io.*; 4 import java.util.*; 5 import java.util.concurrent.ConcurrentHashMap; 6 import java.util.concurrent.ConcurrentSkipListMap; 7 8 import com.fasterxml.jackson.annotation.*; 9 import com.fasterxml.jackson.annotation.JsonTypeInfo.Id; 10 import com.fasterxml.jackson.core.*; 11 import com.fasterxml.jackson.databind.*; 12 import com.fasterxml.jackson.databind.ObjectMapper.DefaultTyping; 13 import com.fasterxml.jackson.databind.annotation.JsonSerialize; 14 import com.fasterxml.jackson.databind.testutil.NoCheckSubTypeValidator; 15 16 @SuppressWarnings("serial") 17 public class TestMapSerialization extends BaseMapTest 18 { 19 @JsonSerialize(using=PseudoMapSerializer.class) 20 static class PseudoMap extends LinkedHashMap<String,String> 21 { PseudoMap(String... values)22 public PseudoMap(String... values) { 23 for (int i = 0, len = values.length; i < len; i += 2) { 24 put(values[i], values[i+1]); 25 } 26 } 27 } 28 29 static class PseudoMapSerializer extends JsonSerializer<Map<String,String>> 30 { 31 @Override serialize(Map<String,String> value, JsonGenerator gen, SerializerProvider provider)32 public void serialize(Map<String,String> value, 33 JsonGenerator gen, SerializerProvider provider) throws IOException 34 { 35 // just use standard Map.toString(), output as JSON String 36 gen.writeString(value.toString()); 37 } 38 } 39 40 // [databind#335] 41 static class MapOrderingBean { 42 @JsonPropertyOrder(alphabetic=true) 43 public LinkedHashMap<String,Integer> map; 44 MapOrderingBean(String... keys)45 public MapOrderingBean(String... keys) { 46 map = new LinkedHashMap<String,Integer>(); 47 int ix = 1; 48 for (String key : keys) { 49 map.put(key, ix++); 50 } 51 } 52 } 53 54 // [databind#565]: Support ser/deser of Map.Entry 55 static class StringIntMapEntry implements Map.Entry<String,Integer> { 56 public final String k; 57 public final Integer v; StringIntMapEntry(String k, Integer v)58 public StringIntMapEntry(String k, Integer v) { 59 this.k = k; 60 this.v = v; 61 } 62 63 @Override getKey()64 public String getKey() { 65 return k; 66 } 67 68 @Override getValue()69 public Integer getValue() { 70 return v; 71 } 72 73 @Override setValue(Integer value)74 public Integer setValue(Integer value) { 75 throw new UnsupportedOperationException(); 76 } 77 } 78 79 static class StringIntMapEntryWrapper { 80 public StringIntMapEntry value; 81 StringIntMapEntryWrapper(String k, Integer v)82 public StringIntMapEntryWrapper(String k, Integer v) { 83 value = new StringIntMapEntry(k, v); 84 } 85 } 86 87 // for [databind#691] 88 @JsonTypeInfo(use=JsonTypeInfo.Id.NAME) 89 @JsonTypeName("mymap") 90 static class MapWithTypedValues extends LinkedHashMap<String,String> { } 91 92 @JsonTypeInfo(use = Id.CLASS) 93 public static class Mixin691 { } 94 95 /* 96 /********************************************************** 97 /* Test methods 98 /********************************************************** 99 */ 100 101 final private ObjectMapper MAPPER = objectMapper(); 102 testUsingObjectWriter()103 public void testUsingObjectWriter() throws IOException 104 { 105 ObjectWriter w = MAPPER.writerFor(Object.class); 106 Map<String,Object> map = new LinkedHashMap<String,Object>(); 107 map.put("a", 1); 108 String json = w.writeValueAsString(map); 109 assertEquals(aposToQuotes("{'a':1}"), json); 110 } 111 testMapSerializer()112 public void testMapSerializer() throws IOException 113 { 114 assertEquals("\"{a=b, c=d}\"", MAPPER.writeValueAsString(new PseudoMap("a", "b", "c", "d"))); 115 } 116 117 // problems with map entries, values testMapKeySetValuesSerialization()118 public void testMapKeySetValuesSerialization() throws IOException 119 { 120 Map<String,String> map = new HashMap<String,String>(); 121 map.put("a", "b"); 122 assertEquals("[\"a\"]", MAPPER.writeValueAsString(map.keySet())); 123 assertEquals("[\"b\"]", MAPPER.writeValueAsString(map.values())); 124 125 // TreeMap has similar inner class(es): 126 map = new TreeMap<String,String>(); 127 map.put("c", "d"); 128 assertEquals("[\"c\"]", MAPPER.writeValueAsString(map.keySet())); 129 assertEquals("[\"d\"]", MAPPER.writeValueAsString(map.values())); 130 131 // and for [JACKSON-533], same for concurrent maps 132 map = new ConcurrentHashMap<String,String>(); 133 map.put("e", "f"); 134 assertEquals("[\"e\"]", MAPPER.writeValueAsString(map.keySet())); 135 assertEquals("[\"f\"]", MAPPER.writeValueAsString(map.values())); 136 } 137 138 // sort Map entries by key testOrderByKey()139 public void testOrderByKey() throws IOException 140 { 141 ObjectMapper m = new ObjectMapper(); 142 assertFalse(m.isEnabled(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS)); 143 LinkedHashMap<String,Integer> map = new LinkedHashMap<String,Integer>(); 144 map.put("b", 3); 145 map.put("a", 6); 146 // by default, no (re)ordering: 147 assertEquals("{\"b\":3,\"a\":6}", m.writeValueAsString(map)); 148 // but can be changed 149 ObjectWriter sortingW = m.writer(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS); 150 assertEquals("{\"a\":6,\"b\":3}", sortingW.writeValueAsString(map)); 151 } 152 153 // related to [databind#1411] testOrderByWithNulls()154 public void testOrderByWithNulls() throws IOException 155 { 156 ObjectWriter sortingW = MAPPER.writer(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS); 157 // 16-Oct-2016, tatu: but mind the null key, if any 158 Map<String,Integer> mapWithNullKey = new LinkedHashMap<String,Integer>(); 159 mapWithNullKey.put(null, 1); 160 mapWithNullKey.put("b", 2); 161 // 16-Oct-2016, tatu: By default, null keys are not accepted... 162 try { 163 /*String json =*/ sortingW.writeValueAsString(mapWithNullKey); 164 //assertEquals(aposToQuotes("{'':1,'b':2}"), json); 165 } catch (JsonMappingException e) { 166 verifyException(e, "Null key for a Map not allowed"); 167 } 168 } 169 170 // [Databind#335] testOrderByKeyViaProperty()171 public void testOrderByKeyViaProperty() throws IOException 172 { 173 MapOrderingBean input = new MapOrderingBean("c", "b", "a"); 174 String json = MAPPER.writeValueAsString(input); 175 assertEquals(aposToQuotes("{'map':{'a':3,'b':2,'c':1}}"), json); 176 } 177 178 // [Databind#565] testMapEntry()179 public void testMapEntry() throws IOException 180 { 181 StringIntMapEntry input = new StringIntMapEntry("answer", 42); 182 String json = MAPPER.writeValueAsString(input); 183 assertEquals(aposToQuotes("{'answer':42}"), json); 184 185 StringIntMapEntry[] array = new StringIntMapEntry[] { input }; 186 json = MAPPER.writeValueAsString(array); 187 assertEquals(aposToQuotes("[{'answer':42}]"), json); 188 189 // and maybe with bit of extra typing? 190 ObjectMapper mapper = new ObjectMapper().activateDefaultTyping(NoCheckSubTypeValidator.instance, 191 DefaultTyping.NON_FINAL); 192 json = mapper.writeValueAsString(input); 193 assertEquals(aposToQuotes("['"+StringIntMapEntry.class.getName()+"',{'answer':42}]"), 194 json); 195 } 196 testMapEntryWrapper()197 public void testMapEntryWrapper() throws IOException 198 { 199 StringIntMapEntryWrapper input = new StringIntMapEntryWrapper("answer", 42); 200 String json = MAPPER.writeValueAsString(input); 201 assertEquals(aposToQuotes("{'value':{'answer':42}}"), json); 202 } 203 204 // [databind#691] testNullJsonMapping691()205 public void testNullJsonMapping691() throws Exception 206 { 207 MapWithTypedValues input = new MapWithTypedValues(); 208 input.put("id", "Test"); 209 input.put("NULL", null); 210 211 String json = MAPPER.writeValueAsString(input); 212 213 assertEquals(aposToQuotes("{'@type':'mymap','id':'Test','NULL':null}"), 214 json); 215 } 216 217 // [databind#691] testNullJsonInTypedMap691()218 public void testNullJsonInTypedMap691() throws Exception { 219 Map<String, String> map = new HashMap<String, String>(); 220 map.put("NULL", null); 221 222 ObjectMapper mapper = new ObjectMapper(); 223 mapper.addMixIn(Object.class, Mixin691.class); 224 String json = mapper.writeValueAsString(map); 225 assertEquals("{\"@class\":\"java.util.HashMap\",\"NULL\":null}", json); 226 } 227 228 // [databind#1513] testConcurrentMaps()229 public void testConcurrentMaps() throws Exception 230 { 231 final ObjectWriter w = MAPPER.writer(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS); 232 233 Map<String,String> input = new ConcurrentSkipListMap<String,String>(); 234 input.put("x", "y"); 235 input.put("a", "b"); 236 String json = w.writeValueAsString(input); 237 assertEquals(aposToQuotes("{'a':'b','x':'y'}"), json); 238 239 input = new ConcurrentHashMap<String,String>(); 240 input.put("x", "y"); 241 input.put("a", "b"); 242 json = w.writeValueAsString(input); 243 assertEquals(aposToQuotes("{'a':'b','x':'y'}"), json); 244 245 // One more: while not technically concurrent map at all, exhibits same issue 246 input = new Hashtable<String,String>(); 247 input.put("x", "y"); 248 input.put("a", "b"); 249 json = w.writeValueAsString(input); 250 assertEquals(aposToQuotes("{'a':'b','x':'y'}"), json); 251 } 252 } 253