Java tutorial
/* * This software is distributed under no licenses and warranty. * Use this software at your own risk. */ package com.github.francescojo.gsondbg; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonIOException; import com.google.gson.JsonObject; import com.google.gson.JsonParser; import com.google.gson.JsonPrimitive; import com.google.gson.JsonSyntaxException; import com.google.gson.TypeAdapter; import com.google.gson.TypeAdapterFactory; import com.google.gson.annotations.SerializedName; import com.google.gson.reflect.TypeToken; import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonWriter; import java.io.IOException; import java.io.Reader; import java.io.Writer; import java.lang.annotation.Annotation; import java.lang.reflect.Field; import java.lang.reflect.Type; import java.util.HashMap; import java.util.Map; import java.util.concurrent.Callable; import java.util.logging.Level; import java.util.logging.Logger; /** * An utility class which helps tracking cause of ClassCastException <em>at one glance</em> while using Google's gson. * <p> * Most methods except {@code fromJson} variations are simple composition methods for source code compatibility. * </p> * * @author Francesco Jo(nimbusob@gmail.com) * @since 14 - Apr - 2016 */ @SuppressWarnings("all") public class GsonDebuggable { private static final boolean IS_DEBUG = true; private final static Logger LOGGER = Logger.getLogger(GsonDebuggable.class.getName()); private static Callable<GsonDebuggable> instanceFactory = null; private GsonBuilder gsonBuilder = null; /** * <a href="https://github.com/google/gson/issues/613">GSON is thread safe</a> */ private Gson gson = new Gson(); /** * Provides an global instance factory to GsonDebuggable. This method is explicitly designed for unit testing. * You really don't need to use this in your business logic. * * @param instanceFactory an GsonDebuggable instance producer. */ protected static void setInstanceFactory(Callable<GsonDebuggable> instanceFactory) { GsonDebuggable.instanceFactory = instanceFactory; } /** * Gets a {@code GsonDebuggable} which is created and cached by VM. Json deserialisation is done by default * Gson instance. * * @return a {@code GsonDebuggable} instance. * @see com.google.gson.Gson#Gson() */ public static GsonDebuggable getInstance() { return getInstanceWith(null); } /** * Gets a {@code GsonDebuggable} which is created and cached by VM. Json deserialisation is done by * * @param builder your own {@link com.google.gson.GsonBuilder} instance. * @return a {@code GsonDebuggable} instance. */ // TODO: test instance caching public static GsonDebuggable getInstanceWith(GsonBuilder builder) { if (null == GsonDebuggable.instanceFactory) { // No equals method on GsonBuilder :( if (null != builder && InstanceHolder.INSTANCE.gsonBuilder != builder) { InstanceHolder.INSTANCE.gson = builder.create(); } return InstanceHolder.INSTANCE; } else { try { return instanceFactory.call(); } catch (Exception e) { throw new RuntimeException(e); } } } /** * See {@link com.google.gson.Gson#fromJson(String, Class)} for its original documentation. */ public <T> T fromJson(final String jsonString, final Class<T> klass) { if (IS_DEBUG) { return fromJsonToClassDebug(jsonString, klass, new Callable<T>() { @Override public T call() throws Exception { return gson.fromJson(jsonString, klass); } }); } else { return gson.fromJson(jsonString, klass); } } /** * See {@link com.google.gson.Gson#fromJson(String, Type)} for its original documentation. */ // TODO: test public <T> T fromJson(String json, Type typeOfT) throws JsonSyntaxException { return fromJson(json, (Class<T>) typeOfT.getClass()); } /** * See {@link com.google.gson.Gson#fromJson(Reader, Class)} for its original documentation. * <p> * Parse error inspection on this method is currently disabled. * Please read 'Limitations of GsonDebuggable' for details. * </p> */ public <T> T fromJson(final Reader json, final Class<T> classOfT) throws JsonSyntaxException, JsonIOException { return gson.fromJson(json, classOfT); } /** * See {@link com.google.gson.Gson#fromJson(Reader, Type)} for its original documentation. * <p> * Parse error inspection on this method is currently disabled. * Please read 'Limitations of GsonDebuggable' for details. * </p> */ public <T> T fromJson(Reader json, Type typeOfT) throws JsonIOException, JsonSyntaxException { return gson.fromJson(json, typeOfT); } /** * See {@link com.google.gson.Gson#fromJson(JsonReader, Type)} for its original documentation. * <p> * Parse error inspection on this method is currently disabled. * Please read 'Limitations of GsonDebuggable' for details. * </p> */ public <T> T fromJson(JsonReader reader, Type typeOfT) throws JsonIOException, JsonSyntaxException { return gson.fromJson(reader, typeOfT); } /** * See {@link com.google.gson.Gson#fromJson(JsonElement, Class)} for its original documentation. */ public <T> T fromJson(JsonElement json, Class<T> classOfT) throws JsonSyntaxException { if (IS_DEBUG) { try { return gson.fromJson(json, classOfT); } catch (JsonSyntaxException e) { return inspectJson(json.toString(), classOfT, e); } } else { return gson.fromJson(json, classOfT); } } /** * See {@link com.google.gson.Gson#fromJson(JsonElement, Type)} for its original documentation. */ // TODO: test public <T> T fromJson(JsonElement json, Type typeOfT) throws JsonSyntaxException { return fromJson(json, (Class<T>) typeOfT.getClass()); } private <T> T fromJsonToClassDebug(String jsonString, Class<T> klass, Callable<T> fromJsonJob) { try { return fromJsonJob.call(); } catch (JsonSyntaxException e1) { return inspectJson(jsonString, klass, e1); } catch (Exception ignore) { // Unreachable in normal - except any mistakes in <code>fromJsonJob</code>. throw new RuntimeException(ignore); } } private <T> T inspectJson(String jsonString, Class<T> klass, RuntimeException e) { JsonElement json; try { json = new JsonParser().parse(jsonString); } catch (JsonSyntaxException e2) { log("Unable to parse given string as JsonElement. Given string:\n%s", jsonString); throw e; } if (json instanceof JsonArray) { log("Error while mapping given JsonString as %s. Given string:\n%s", klass.getCanonicalName(), jsonString); } else if (json instanceof JsonObject) { JsonObject jsonObj = json.getAsJsonObject(); inspectJsonObject(jsonObj, klass); } throw e; } private void inspectJsonObject(JsonObject jsonObject, Class<?> klass) { Map<String, Field> mappingInfo = new HashMap<String, Field>(); for (Field classField : klass.getDeclaredFields()) { Annotation[] annotations = classField.getAnnotations(); if (null == annotations || 0 == annotations.length) { mappingInfo.put(classField.getName(), classField); } else { for (Annotation annotation : annotations) { if (annotation instanceof SerializedName) { String customName = ((SerializedName) annotation).value(); mappingInfo.put(customName, classField); /* * FIXME: alternate() is introduced in Gson 2.4. Safe to delete if your Gson is lower than 2.4. */ try { String[] alternateNames = ((SerializedName) annotation).alternate(); for (String alternateName : alternateNames) { mappingInfo.put(alternateName, classField); } } catch (NoSuchMethodError ignore) { } } } } } for (Map.Entry<String, JsonElement> entry : jsonObject.entrySet()) { String key = entry.getKey(); JsonElement value = entry.getValue(); if (!mappingInfo.containsKey(key)) { /* * Gson ignores fields that are not found in json by default - following same rule in here. */ continue; } Field javaField = mappingInfo.get(key); Class<?> javaType = javaField.getType(); if (WellKnownTypeCastingRules.isWellKnown(javaType)) { if (!(value instanceof JsonPrimitive)) { log("%s#%s is declared as ''%s''; however JSON is: { \"%s\": %s }", javaField.getDeclaringClass().getName(), javaField.getName(), javaField.getType().getCanonicalName(), key, value); continue; } WellKnownTypeCastingRules rule = WellKnownTypeCastingRules.byJavaType(javaType); JsonPrimitive primitive = value.getAsJsonPrimitive(); if (rule.isAcceptable(primitive)) { continue; } log("%s#%s is declared as ''%s''; however JSON is: { \"%s\": %s }", javaField.getDeclaringClass().getName(), javaField.getName(), javaField.getType().getCanonicalName(), key, value); } else if (javaType.isArray()) { try { gson.fromJson(value, javaType); } catch (JsonSyntaxException e) { log("%s#%s is declared as ''%s''; however JSON is: { \"%s\": %s }", javaField.getDeclaringClass().getName(), javaField.getName(), javaField.getType().getCanonicalName(), key, value); } } else if (value.isJsonObject()) { inspectJsonObject(value.getAsJsonObject(), javaType); } } } private static void log(String msg, Object... args) { LOGGER.log(Level.WARNING, String.format(msg, args)); } enum WellKnownTypeCastingRules { INT(int.class, new Predicate<JsonPrimitive>() { @Override public boolean evaluate(JsonPrimitive input) { try { input.getAsInt(); return true; } catch (NumberFormatException e) { return false; } } }), LONG(long.class, new Predicate<JsonPrimitive>() { @Override public boolean evaluate(JsonPrimitive input) { try { input.getAsLong(); return true; } catch (NumberFormatException e) { return false; } } }), SHORT(short.class, new Predicate<JsonPrimitive>() { @Override public boolean evaluate(JsonPrimitive input) { try { input.getAsShort(); return true; } catch (NumberFormatException e) { return false; } } }), FLOAT(float.class, new Predicate<JsonPrimitive>() { @Override public boolean evaluate(JsonPrimitive input) { try { input.getAsFloat(); return true; } catch (NumberFormatException e) { return false; } } }), DOUBLE(double.class, new Predicate<JsonPrimitive>() { @Override public boolean evaluate(JsonPrimitive input) { try { input.getAsDouble(); return true; } catch (NumberFormatException e) { return false; } } }), BYTE(byte.class, new Predicate<JsonPrimitive>() { @Override public boolean evaluate(JsonPrimitive input) { try { input.getAsByte(); return true; } catch (NumberFormatException e) { return false; } } }), BOOLEAN(boolean.class, new Predicate<JsonPrimitive>() { @Override public boolean evaluate(JsonPrimitive input) { return input.isBoolean(); } }), CHAR(char.class, new Predicate<JsonPrimitive>() { @Override public boolean evaluate(JsonPrimitive input) { /* * There is no Character type in Json-Javascript; however gson will not convert string literals that * are longer than 2 letters as char. */ return input.isString() && input.getAsString().length() == 1; } }), BOXED_INT(Integer.class, INT.predicate), BOXED_LONG(Long.class, LONG.predicate), BOXED_SHORT( Short.class, SHORT.predicate), BOXED_FLOAT(Float.class, FLOAT.predicate), BOXED_DOUBLE(Double.class, DOUBLE.predicate), BOXED_BYTE(Byte.class, BYTE.predicate), BOXED_BOOLEAN(Boolean.class, BOOLEAN.predicate), BOXED_CHAR(Character.class, CHAR.predicate), STRING(String.class, new Predicate<JsonPrimitive>() { @Override public boolean evaluate(JsonPrimitive input) { return input.isString(); } }); final Class<?> type; final Predicate<JsonPrimitive> predicate; WellKnownTypeCastingRules(Class<?> c, Predicate<JsonPrimitive> p) { this.type = c; this.predicate = p; } static WellKnownTypeCastingRules byJavaType(Class<?> klass) { for (WellKnownTypeCastingRules r : WellKnownTypeCastingRules.values()) { if (r.type == klass) { return r; } } return STRING; } static boolean isWellKnown(Class<?> klass) { for (WellKnownTypeCastingRules r : WellKnownTypeCastingRules.values()) { if (r.type == klass) { return true; } } return false; } boolean isAcceptable(JsonPrimitive value) { return predicate.evaluate(value); } } private static class InstanceHolder { private static final GsonDebuggable INSTANCE = new GsonDebuggable(); } /** * See {@link com.google.gson.Gson#getAdapter(TypeToken)} for its original documentation. */ public <T> TypeAdapter<T> getAdapter(TypeToken<T> type) { return gson.getAdapter(type); } /** * See {@link com.google.gson.Gson#getDelegateAdapter(TypeAdapterFactory, TypeToken)} * for its original documentation. */ public <T> TypeAdapter<T> getDelegateAdapter(TypeAdapterFactory skipPast, TypeToken<T> type) { return gson.getDelegateAdapter(skipPast, type); } /** * See {@link com.google.gson.Gson#getAdapter(Class)} for its original documentation. */ public <T> TypeAdapter<T> getAdapter(Class<T> type) { return gson.getAdapter(type); } /** * See {@link com.google.gson.Gson#toJsonTree(Object)} for its original documentation. */ public JsonElement toJsonTree(Object src) { return gson.toJsonTree(src); } /** * See {@link com.google.gson.Gson#toJsonTree(Object, Type)} for its original documentation. */ public JsonElement toJsonTree(Object src, Type typeOfSrc) { return gson.toJsonTree(src, typeOfSrc); } /** * See {@link com.google.gson.Gson#toJson(Object)} for its original documentation. */ public String toJson(Object src) { return gson.toJson(src); } /** * See {@link com.google.gson.Gson#toJson(Object, Type)} for its original documentation. */ public String toJson(Object src, Type typeOfSrc) { return gson.toJson(src, typeOfSrc); } /** * See {@link com.google.gson.Gson#toJson(Object, Appendable)} for its original documentation. */ public void toJson(Object src, Appendable writer) throws JsonIOException { gson.toJson(src, writer); } /** * See {@link com.google.gson.Gson#toJson(Object, Type, Appendable)} for its original documentation. */ public void toJson(Object src, Type typeOfSrc, Appendable writer) throws JsonIOException { gson.toJson(src, typeOfSrc, writer); } /** * See {@link com.google.gson.Gson#toJson(Object, Type, JsonWriter)} for its original documentation. */ public void toJson(Object src, Type typeOfSrc, JsonWriter writer) throws JsonIOException { gson.toJson(src, typeOfSrc, writer); } /** * See {@link com.google.gson.Gson#toJson(JsonElement)} for its original documentation. */ public String toJson(JsonElement jsonElement) { return gson.toJson(jsonElement); } /** * See {@link com.google.gson.Gson#toJson(JsonElement, Appendable)} for its original documentation. */ public void toJson(JsonElement jsonElement, Appendable writer) throws JsonIOException { gson.toJson(jsonElement, writer); } /** * See {@link com.google.gson.Gson#toJson(JsonElement, JsonWriter)} for its original documentation. */ public void toJson(JsonElement jsonElement, JsonWriter writer) throws JsonIOException { gson.toJson(jsonElement, writer); } /** * See {@link com.google.gson.Gson#newJsonWriter(Writer)} for its original documentation. * <p> * FIXME: This method is introduced in Gson 2.4. Safe to delete if your Gson is lower than 2.4. * </p> */ public JsonWriter newJsonWriter(Writer writer) throws IOException { return gson.newJsonWriter(writer); } /** * See {@link com.google.gson.Gson#newJsonReader(Reader)} for its original documentation. * <p> * FIXME: This method is introduced in Gson 2.6. Safe to delete if your Gson is lower than 2.6. * </p> */ public JsonReader newJsonReader(Reader reader) { return gson.newJsonReader(reader); } @Override public String toString() { return gson.toString(); } private interface Predicate<I> { boolean evaluate(I input); } }