| 1 | /* |
| 2 | * Copyright (C) 2011 Google Inc. |
| 3 | * |
| 4 | * Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | * you may not use this file except in compliance with the License. |
| 6 | * You may obtain a copy of the License at |
| 7 | * |
| 8 | * http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | * |
| 10 | * Unless required by applicable law or agreed to in writing, software |
| 11 | * distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | * See the License for the specific language governing permissions and |
| 14 | * limitations under the License. |
| 15 | */ |
| 16 | |
| 17 | package com.google.gson.internal.bind; |
| 18 | |
| 19 | import com.google.gson.Gson; |
| 20 | import com.google.gson.JsonElement; |
| 21 | import com.google.gson.JsonPrimitive; |
| 22 | import com.google.gson.JsonSyntaxException; |
| 23 | import com.google.gson.TypeAdapter; |
| 24 | import com.google.gson.TypeAdapterFactory; |
| 25 | import com.google.gson.internal.$Gson$Types; |
| 26 | import com.google.gson.internal.ConstructorConstructor; |
| 27 | import com.google.gson.internal.JsonReaderInternalAccess; |
| 28 | import com.google.gson.internal.ObjectConstructor; |
| 29 | import com.google.gson.internal.Streams; |
| 30 | import com.google.gson.reflect.TypeToken; |
| 31 | import com.google.gson.stream.JsonReader; |
| 32 | import com.google.gson.stream.JsonToken; |
| 33 | import com.google.gson.stream.JsonWriter; |
| 34 | import java.io.IOException; |
| 35 | import java.lang.reflect.Type; |
| 36 | import java.util.ArrayList; |
| 37 | import java.util.List; |
| 38 | import java.util.Map; |
| 39 | |
| 40 | /** |
| 41 | * Adapts maps to either JSON objects or JSON arrays. |
| 42 | * |
| 43 | * <h3>Maps as JSON objects</h3> |
| 44 | * For primitive keys or when complex map key serialization is not enabled, this |
| 45 | * converts Java {@link Map Maps} to JSON Objects. This requires that map keys |
| 46 | * can be serialized as strings; this is insufficient for some key types. For |
| 47 | * example, consider a map whose keys are points on a grid. The default JSON |
| 48 | * form encodes reasonably: <pre> {@code |
| 49 | * Map<Point, String> original = new LinkedHashMap<Point, String>(); |
| 50 | * original.put(new Point(5, 6), "a"); |
| 51 | * original.put(new Point(8, 8), "b"); |
| 52 | * System.out.println(gson.toJson(original, type)); |
| 53 | * }</pre> |
| 54 | * The above code prints this JSON object:<pre> {@code |
| 55 | * { |
| 56 | * "(5,6)": "a", |
| 57 | * "(8,8)": "b" |
| 58 | * } |
| 59 | * }</pre> |
| 60 | * But GSON is unable to deserialize this value because the JSON string name is |
| 61 | * just the {@link Object#toString() toString()} of the map key. Attempting to |
| 62 | * convert the above JSON to an object fails with a parse exception: |
| 63 | * <pre>com.google.gson.JsonParseException: Expecting object found: "(5,6)" |
| 64 | * at com.google.gson.JsonObjectDeserializationVisitor.visitFieldUsingCustomHandler |
| 65 | * at com.google.gson.ObjectNavigator.navigateClassFields |
| 66 | * ...</pre> |
| 67 | * |
| 68 | * <h3>Maps as JSON arrays</h3> |
| 69 | * An alternative approach taken by this type adapter when it is required and |
| 70 | * complex map key serialization is enabled is to encode maps as arrays of map |
| 71 | * entries. Each map entry is a two element array containing a key and a value. |
| 72 | * This approach is more flexible because any type can be used as the map's key; |
| 73 | * not just strings. But it's also less portable because the receiver of such |
| 74 | * JSON must be aware of the map entry convention. |
| 75 | * |
| 76 | * <p>Register this adapter when you are creating your GSON instance. |
| 77 | * <pre> {@code |
| 78 | * Gson gson = new GsonBuilder() |
| 79 | * .registerTypeAdapter(Map.class, new MapAsArrayTypeAdapter()) |
| 80 | * .create(); |
| 81 | * }</pre> |
| 82 | * This will change the structure of the JSON emitted by the code above. Now we |
| 83 | * get an array. In this case the arrays elements are map entries: |
| 84 | * <pre> {@code |
| 85 | * [ |
| 86 | * [ |
| 87 | * { |
| 88 | * "x": 5, |
| 89 | * "y": 6 |
| 90 | * }, |
| 91 | * "a", |
| 92 | * ], |
| 93 | * [ |
| 94 | * { |
| 95 | * "x": 8, |
| 96 | * "y": 8 |
| 97 | * }, |
| 98 | * "b" |
| 99 | * ] |
| 100 | * ] |
| 101 | * }</pre> |
| 102 | * This format will serialize and deserialize just fine as long as this adapter |
| 103 | * is registered. |
| 104 | */ |
| 105 | public final class MapTypeAdapterFactory implements TypeAdapterFactory { |
| 106 | private final ConstructorConstructor constructorConstructor; |
| 107 | private final boolean complexMapKeySerialization; |
| 108 | |
| 109 | public MapTypeAdapterFactory(ConstructorConstructor constructorConstructor, |
| 110 | boolean complexMapKeySerialization) { |
| 111 | this.constructorConstructor = constructorConstructor; |
| 112 | this.complexMapKeySerialization = complexMapKeySerialization; |
| 113 | } |
| 114 | |
| 115 | public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> typeToken) { |
| 116 | Type type = typeToken.getType(); |
| 117 | |
| 118 | Class<? super T> rawType = typeToken.getRawType(); |
| 119 | if (!Map.class.isAssignableFrom(rawType)) { |
| 120 | return null; |
| 121 | } |
| 122 | |
| 123 | Class<?> rawTypeOfSrc = $Gson$Types.getRawType(type); |
| 124 | Type[] keyAndValueTypes = $Gson$Types.getMapKeyAndValueTypes(type, rawTypeOfSrc); |
| 125 | TypeAdapter<?> keyAdapter = getKeyAdapter(gson, keyAndValueTypes[0]); |
| 126 | TypeAdapter<?> valueAdapter = gson.getAdapter(TypeToken.get(keyAndValueTypes[1])); |
| 127 | ObjectConstructor<T> constructor = constructorConstructor.get(typeToken); |
| 128 | |
| 129 | @SuppressWarnings({"unchecked", "rawtypes"}) |
| 130 | // we don't define a type parameter for the key or value types |
| 131 | TypeAdapter<T> result = new Adapter(gson, keyAndValueTypes[0], keyAdapter, |
| 132 | keyAndValueTypes[1], valueAdapter, constructor); |
| 133 | return result; |
| 134 | } |
| 135 | |
| 136 | /** |
| 137 | * Returns a type adapter that writes the value as a string. |
| 138 | */ |
| 139 | private TypeAdapter<?> getKeyAdapter(Gson context, Type keyType) { |
| 140 | return (keyType == boolean.class || keyType == Boolean.class) |
| 141 | ? TypeAdapters.BOOLEAN_AS_STRING |
| 142 | : context.getAdapter(TypeToken.get(keyType)); |
| 143 | } |
| 144 | |
| 145 | private final class Adapter<K, V> extends TypeAdapter<Map<K, V>> { |
| 146 | private final TypeAdapter<K> keyTypeAdapter; |
| 147 | private final TypeAdapter<V> valueTypeAdapter; |
| 148 | private final ObjectConstructor<? extends Map<K, V>> constructor; |
| 149 | |
| 150 | public Adapter(Gson context, Type keyType, TypeAdapter<K> keyTypeAdapter, |
| 151 | Type valueType, TypeAdapter<V> valueTypeAdapter, |
| 152 | ObjectConstructor<? extends Map<K, V>> constructor) { |
| 153 | this.keyTypeAdapter = |
| 154 | new TypeAdapterRuntimeTypeWrapper<K>(context, keyTypeAdapter, keyType); |
| 155 | this.valueTypeAdapter = |
| 156 | new TypeAdapterRuntimeTypeWrapper<V>(context, valueTypeAdapter, valueType); |
| 157 | this.constructor = constructor; |
| 158 | } |
| 159 | |
| 160 | public Map<K, V> read(JsonReader in) throws IOException { |
| 161 | JsonToken peek = in.peek(); |
| 162 | if (peek == JsonToken.NULL) { |
| 163 | in.nextNull(); |
| 164 | return null; |
| 165 | } |
| 166 | |
| 167 | Map<K, V> map = constructor.construct(); |
| 168 | |
| 169 | if (peek == JsonToken.BEGIN_ARRAY) { |
| 170 | in.beginArray(); |
| 171 | while (in.hasNext()) { |
| 172 | in.beginArray(); // entry array |
| 173 | K key = keyTypeAdapter.read(in); |
| 174 | V value = valueTypeAdapter.read(in); |
| 175 | V replaced = map.put(key, value); |
| 176 | if (replaced != null) { |
| 177 | throw new JsonSyntaxException("duplicate key: " + key); |
| 178 | } |
| 179 | in.endArray(); |
| 180 | } |
| 181 | in.endArray(); |
| 182 | } else { |
| 183 | in.beginObject(); |
| 184 | while (in.hasNext()) { |
| 185 | JsonReaderInternalAccess.INSTANCE.promoteNameToValue(in); |
| 186 | K key = keyTypeAdapter.read(in); |
| 187 | V value = valueTypeAdapter.read(in); |
| 188 | V replaced = map.put(key, value); |
| 189 | if (replaced != null) { |
| 190 | throw new JsonSyntaxException("duplicate key: " + key); |
| 191 | } |
| 192 | } |
| 193 | in.endObject(); |
| 194 | } |
| 195 | return map; |
| 196 | } |
| 197 | |
| 198 | public void write(JsonWriter out, Map<K, V> map) throws IOException { |
| 199 | if (map == null) { |
| 200 | out.nullValue(); |
| 201 | return; |
| 202 | } |
| 203 | |
| 204 | if (!complexMapKeySerialization) { |
| 205 | out.beginObject(); |
| 206 | for (Map.Entry<K, V> entry : map.entrySet()) { |
| 207 | out.name(String.valueOf(entry.getKey())); |
| 208 | valueTypeAdapter.write(out, entry.getValue()); |
| 209 | } |
| 210 | out.endObject(); |
| 211 | return; |
| 212 | } |
| 213 | |
| 214 | boolean hasComplexKeys = false; |
| 215 | List<JsonElement> keys = new ArrayList<JsonElement>(map.size()); |
| 216 | |
| 217 | List<V> values = new ArrayList<V>(map.size()); |
| 218 | for (Map.Entry<K, V> entry : map.entrySet()) { |
| 219 | JsonElement keyElement = keyTypeAdapter.toJsonTree(entry.getKey()); |
| 220 | keys.add(keyElement); |
| 221 | values.add(entry.getValue()); |
| 222 | hasComplexKeys |= keyElement.isJsonArray() || keyElement.isJsonObject(); |
| 223 | } |
| 224 | |
| 225 | if (hasComplexKeys) { |
| 226 | out.beginArray(); |
| 227 | for (int i = 0; i < keys.size(); i++) { |
| 228 | out.beginArray(); // entry array |
| 229 | Streams.write(keys.get(i), out); |
| 230 | valueTypeAdapter.write(out, values.get(i)); |
| 231 | out.endArray(); |
| 232 | } |
| 233 | out.endArray(); |
| 234 | } else { |
| 235 | out.beginObject(); |
| 236 | for (int i = 0; i < keys.size(); i++) { |
| 237 | JsonElement keyElement = keys.get(i); |
| 238 | out.name(keyToString(keyElement)); |
| 239 | valueTypeAdapter.write(out, values.get(i)); |
| 240 | } |
| 241 | out.endObject(); |
| 242 | } |
| 243 | } |
| 244 | |
| 245 | private String keyToString(JsonElement keyElement) { |
| 246 | if (keyElement.isJsonPrimitive()) { |
| 247 | JsonPrimitive primitive = keyElement.getAsJsonPrimitive(); |
| 248 | if (primitive.isNumber()) { |
| 249 | return String.valueOf(primitive.getAsNumber()); |
| 250 | } else if (primitive.isBoolean()) { |
| 251 | return Boolean.toString(primitive.getAsBoolean()); |
| 252 | } else if (primitive.isString()) { |
| 253 | return primitive.getAsString(); |
| 254 | } else { |
| 255 | throw new AssertionError(); |
| 256 | } |
| 257 | } else if (keyElement.isJsonNull()) { |
| 258 | return "null"; |
| 259 | } else { |
| 260 | throw new AssertionError(); |
| 261 | } |
| 262 | } |
| 263 | } |
| 264 | } |