/*
* Copyright (C) 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.smartandroid.sa.json;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* Adapts maps containing complex keys as arrays of map entries.
*
* <h3>Maps as JSON objects</h3> The standard GSON map type adapter converts
* Java {@link Map Maps} to JSON Objects. This requires that map keys can be
* serialized as strings; this is insufficient for some key types. For example,
* consider a map whose keys are points on a grid. The default JSON form encodes
* reasonably:
*
* <pre>
* {
* @code
* Map<Point, String> original = new LinkedHashMap<Point, String>();
* original.put(new Point(5, 6), "a");
* original.put(new Point(8, 8), "b");
* System.out.println(gson.toJson(original, type));
* }
* </pre>
*
* The above code prints this JSON object:
*
* <pre>
* {@code
* {
* "(5,6)": "a",
* "(8,8)": "b"
* }
* }
* </pre>
*
* But GSON is unable to deserialize this value because the JSON string name is
* just the {@link Object#toString() toString()} of the map key. Attempting to
* convert the above JSON to an object fails with a parse exception:
*
* <pre>
* com.google.gson.JsonParseException: Expecting object found: "(5,6)"
* at com.google.gson.JsonObjectDeserializationVisitor.visitFieldUsingCustomHandler
* at com.google.gson.ObjectNavigator.navigateClassFields
* ...
* </pre>
*
* <h3>Maps as JSON arrays</h3> An alternative approach taken by this type
* adapter is to encode maps as arrays of map entries. Each map entry is a two
* element array containing a key and a value. This approach is more flexible
* because any type can be used as the map's key; not just strings. But it's
* also less portable because the receiver of such JSON must be aware of the map
* entry convention.
*
* <p>
* Register this adapter when you are creating your GSON instance.
*
* <pre>
* {
* @code
* Gson gson = new GsonBuilder().registerTypeAdapter(Map.class,
* new MapAsArrayTypeAdapter()).create();
* }
* </pre>
*
* This will change the structure of the JSON emitted by the code above. Now we
* get an array. In this case the arrays elements are map entries:
*
* <pre>
* {@code
* [
* [
* {
* "x": 5,
* "y": 6
* },
* "a",
* ],
* [
* {
* "x": 8,
* "y": 8
* },
* "b"
* ]
* ]
* }
* </pre>
*
* This format will serialize and deserialize just fine as long as this adapter
* is registered.
*
* <p>
* This adapter returns regular JSON objects for maps whose keys are not
* complex. A key is complex if its JSON-serialized form is an array or an
* object.
*/
final class MapAsArrayTypeAdapter extends BaseMapTypeAdapter implements
JsonSerializer<Map<?, ?>>, JsonDeserializer<Map<?, ?>> {
public Map<?, ?> deserialize(JsonElement json, Type typeOfT,
JsonDeserializationContext context) throws JsonParseException {
Map<Object, Object> result = constructMapType(typeOfT, context);
Type[] keyAndValueType = typeToTypeArguments(typeOfT);
if (json.isJsonArray()) {
JsonArray array = json.getAsJsonArray();
for (int i = 0; i < array.size(); i++) {
JsonArray entryArray = array.get(i).getAsJsonArray();
Object k = context.deserialize(entryArray.get(0),
keyAndValueType[0]);
Object v = context.deserialize(entryArray.get(1),
keyAndValueType[1]);
result.put(k, v);
}
checkSize(array, array.size(), result, result.size());
} else {
JsonObject object = json.getAsJsonObject();
for (Map.Entry<String, JsonElement> entry : object.entrySet()) {
Object k = context.deserialize(
new JsonPrimitive(entry.getKey()), keyAndValueType[0]);
Object v = context.deserialize(entry.getValue(),
keyAndValueType[1]);
result.put(k, v);
}
checkSize(object, object.entrySet().size(), result, result.size());
}
return result;
}
public JsonElement serialize(Map<?, ?> src, Type typeOfSrc,
JsonSerializationContext context) {
Type[] keyAndValueType = typeToTypeArguments(typeOfSrc);
boolean serializeAsArray = false;
List<JsonElement> keysAndValues = new ArrayList<JsonElement>();
for (Map.Entry<?, ?> entry : src.entrySet()) {
JsonElement key = serialize(context, entry.getKey(),
keyAndValueType[0]);
serializeAsArray |= key.isJsonObject() || key.isJsonArray();
keysAndValues.add(key);
keysAndValues.add(serialize(context, entry.getValue(),
keyAndValueType[1]));
}
if (serializeAsArray) {
JsonArray result = new JsonArray();
for (int i = 0; i < keysAndValues.size(); i += 2) {
JsonArray entryArray = new JsonArray();
entryArray.add(keysAndValues.get(i));
entryArray.add(keysAndValues.get(i + 1));
result.add(entryArray);
}
return result;
} else {
JsonObject result = new JsonObject();
for (int i = 0; i < keysAndValues.size(); i += 2) {
result.add(keysAndValues.get(i).getAsString(),
keysAndValues.get(i + 1));
}
checkSize(src, src.size(), result, result.entrySet().size());
return result;
}
}
private Type[] typeToTypeArguments(Type typeOfT) {
if (typeOfT instanceof ParameterizedType) {
Type[] typeArguments = ((ParameterizedType) typeOfT)
.getActualTypeArguments();
if (typeArguments.length != 2) {
throw new IllegalArgumentException(
"MapAsArrayTypeAdapter cannot handle " + typeOfT);
}
return typeArguments;
}
return new Type[] { Object.class, Object.class };
}
private void checkSize(Object input, int inputSize, Object output,
int outputSize) {
if (inputSize != outputSize) {
throw new JsonSyntaxException("Input size " + inputSize
+ " != output size " + outputSize + " for input " + input
+ " and output " + output);
}
}
}