]>
Commit | Line | Data |
---|---|---|
cfd903b6 MG |
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 | } |