com.addthis.codec.json.CodecJSON Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of codec Show documentation
Show all versions of codec Show documentation
Codec serialization library
/*
* 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.addthis.codec.json;
import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Modifier;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import com.addthis.basis.util.Bytes;
import com.addthis.codec.Codec;
import com.addthis.codec.codables.Codable;
import com.addthis.codec.codables.ConcurrentCodable;
import com.addthis.codec.codables.SuperCodable;
import com.addthis.codec.config.CodecConfig;
import com.addthis.codec.plugins.PluginRegistry;
import com.addthis.codec.plugins.Plugins;
import com.addthis.codec.reflection.CodableClassInfo;
import com.addthis.codec.reflection.CodableFieldInfo;
import com.addthis.codec.reflection.Fields;
import com.addthis.codec.util.CodableStatistics;
import com.addthis.maljson.JSONArray;
import com.addthis.maljson.JSONException;
import com.addthis.maljson.JSONObject;
import com.addthis.maljson.LineNumberInfo;
import com.typesafe.config.Config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public final class CodecJSON extends Codec {
private CodecJSON() {}
private static final Logger log = LoggerFactory.getLogger(CodecJSON.class);
public static final CodecJSON INSTANCE = new CodecJSON();
@Override
public byte[] encode(Object obj) throws Exception {
return Bytes.toBytes(encodeString(obj));
}
@Override
public CodableStatistics statistics(Object obj) {
throw new UnsupportedOperationException();
}
@Override
public T decode(T shell, byte[] data) throws CodecExceptionLineNumber, JSONException {
return decodeString(shell, Bytes.toString(data));
}
public T decode(T shell, byte[] data, List warnings)
throws Exception {
return decodeString(shell, Bytes.toString(data), warnings);
}
@Override
public boolean storesNull(byte[] data) {
throw new UnsupportedOperationException();
}
public static JSONObject encodeJSON(Object object) throws Exception {
return (JSONObject) encodeObject(object);
}
public static String encodeString(Object object) {
return encodeString(object, 0);
}
public static String encodeString(Object object, int nest) {
try {
Object ret = encodeObject(object);
if (ret instanceof JSONObject) {
return nest > 0 ? ((JSONObject) ret).toString(nest) : ((JSONObject) ret).toString();
} else {
return ret.toString();
}
} catch (RuntimeException ex) {
throw ex;
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}
private static Object encodeArray(Object value) throws Exception {
JSONArray arr = new JSONArray();
int len = Array.getLength(value);
for (int i = 0; i < len; i++) {
arr.put(encodeObject(Array.get(value, i)));
}
return arr;
}
private static Object encodeObject(Object object) throws Exception {
if (object == null) {
return JSONObject.NULL;
}
if (object.getClass().isArray()) {
return encodeArray(object);
}
if (object instanceof JSONCodable) {
return ((JSONCodable) object).toJSONObject();
}
JSONObject obj = null;
boolean lock = object instanceof ConcurrentCodable;
if (lock) {
((ConcurrentCodable) object).encodeLock();
}
try {
if (object instanceof SuperCodable) {
((SuperCodable) object).preEncode();
}
CodableClassInfo classInfo = Fields.getClassFieldMap(object.getClass());
if (classInfo.size() == 0 && !(object instanceof Codable)) {
return object;
}
obj = new JSONObject();
String altType = classInfo.getClassName(object);
if (altType != null) {
obj.put(classInfo.getClassField(), altType);
}
for (Iterator fields = classInfo.values().iterator(); fields.hasNext();) {
CodableFieldInfo field = fields.next();
Object value = field.get(object);
if (value == null || value == JSONObject.NULL || field.isReadOnly()) {
continue;
}
if (JSONCodable.class.isAssignableFrom(field.getTypeOrComponentType())) {
value = ((JSONCodable) value).toJSONObject();
obj.put(field.getName(), value);
} else if (field.isArray()) {
obj.put(field.getName(), encodeArray(value));
} else if (field.isMap()) {
Map, ?> map = (Map, ?>) value;
JSONObject jmap = new JSONObject();
for (Entry, ?> entry : map.entrySet()) {
Object mval = entry.getValue();
// TODO fails with null keys
jmap.put(entry.getKey().toString(), encodeObject(mval));
}
obj.put(field.getName(), jmap);
} else if (field.isCollection()) {
JSONArray jarr = new JSONArray();
for (Iterator> iter = ((Collection>) value).iterator(); iter.hasNext();) {
jarr.put(encodeObject(iter.next()));
}
obj.put(field.getName(), jarr);
} else if (field.isCodable()) {
obj.put(field.getName(), encodeObject(value));
} else if (field.isEnum()) {
obj.put(field.getName(), value.toString());
} else if (field.isNative()) {
obj.put(field.getName(), value);
} else {
System.out.println("unmatched field '" + field.getName() + "' = " + field);
}
}
} finally {
if (lock) {
((ConcurrentCodable) object).encodeUnlock();
}
}
return obj;
}
public static T decodeString(T object, String json)
throws CodecExceptionLineNumber, JSONException {
return decodeJSON(object, new JSONObject(json));
}
public static T decodeString(T object, String json, List warnings)
throws CodecExceptionLineNumber, JSONException {
JSONObject jsonObj = new JSONObject(json);
return decodeJSONInternal(Fields.getClassFieldMap(object.getClass()), object, jsonObj, warnings);
}
public static T decodeArray(Class type, Object object)
throws CodecExceptionLineNumber, JSONException {
return decodeArrayInternal(type, object, LineNumberInfo.MissingInfo, null);
}
public static T decodeArray(Class type, Object object, List warnings)
throws CodecExceptionLineNumber, JSONException {
return decodeArrayInternal(type, object, LineNumberInfo.MissingInfo, warnings);
}
private static T decodeArrayInternal(Class type, Object object, LineNumberInfo info, List warnings)
throws CodecExceptionLineNumber, JSONException {
if (object == null || object == JSONObject.NULL) {
return null;
}
if (object.getClass() != JSONArray.class) {
throw new CodecExceptionLineNumber(object.toString() + " not an instance of JSONArray for class " + type, info);
}
JSONArray array = (JSONArray) object;
T value = (T) Array.newInstance(type, array.length());
if (type == byte.class || type == Byte.class) {
for (int i = 0; i < array.length(); i++) {
Array.set(value, i, (byte) array.getInt(i));
}
} else {
for (int i = 0; i < array.length(); i++) {
Object element = decodeObjectInternal(type, array.opt(i), array.getLineNumber(i), warnings);
try {
Array.set(value, i, element);
} catch (IllegalArgumentException ex) {
throw new CodecExceptionLineNumber("Element " + i + " with value " +
array.opt(i).toString() + " cannot be converted to " + type.toString(),
array.getLineNumber(i));
}
}
}
return value;
}
@SuppressWarnings("unchecked")
public static T decodeObject(Class type, Object json)
throws CodecExceptionLineNumber, JSONException {
return decodeObjectInternal(type, json, LineNumberInfo.MissingInfo, null);
}
@SuppressWarnings("unchecked")
public static T decodeObject(Class type, Object json, List warnings)
throws CodecExceptionLineNumber, JSONException {
return decodeObjectInternal(type, json, LineNumberInfo.MissingInfo, warnings);
}
@SuppressWarnings("unchecked")
public static T decodeObjectInternal(Class type, Object json,
LineNumberInfo info, List warnings)
throws CodecExceptionLineNumber, JSONException {
if ((json == null) || (json == JSONObject.NULL)) {
return null;
}
if (type == json.getClass()) {
return (T) json;
}
if (Number.class.isAssignableFrom(type)) {
Number num = (Number) json;
if (type == Short.class) {
json = new Short(num.shortValue());
} else if (type == Integer.class) {
json = new Integer(num.intValue());
} else if (type == Long.class) {
json = new Long(num.longValue());
} else if (type == Float.class) {
json = new Float(num.floatValue());
} else if (type == Double.class) {
json = new Double(num.doubleValue());
} else if (type == AtomicInteger.class) {
json = new AtomicInteger(num.intValue());
} else if (type == AtomicLong.class) {
json = new AtomicLong(num.longValue());
}
return (T) json;
} else if (type.isEnum()) {
/**
* Attempting to invoke {@link Enum#valueOf(Class, String)}
* fails in the downstream assignment on
* {@link Array#set(Object, int, Object)} so reflection is used
* instead.
*/
try {
json = type.getMethod("valueOf", String.class).invoke(null, json.toString().toUpperCase());
} catch (InvocationTargetException ex) {
throw new CodecExceptionLineNumber("Could not convert the string \"" + json.toString() +
"\" to the Enum type " + type.getName(), info);
} catch (NoSuchMethodException | IllegalAccessException ex) {
throw new IllegalStateException("Attempted to decode enum type", ex);
}
return (T) json;
}
CodableClassInfo classInfo = Fields.getClassFieldMap(type);
// json config is "unexpectedly" an array; if the base class has registered a handler, use it
if (json instanceof JSONArray) {
Class> arrarySugar = classInfo.getArraySugar();
if (arrarySugar != null) {
LineNumberInfo infoCopy = ((JSONArray) json).getMyLineNumberInfo();
JSONObject magicWrapper = new JSONObject();
magicWrapper.put(classInfo.getPluginMap().aliasDefaults("_array").toConfig().getString("_primary"), json, infoCopy, infoCopy);
classInfo = Fields.getClassFieldMap(arrarySugar);
json = magicWrapper;
type = (Class) arrarySugar;
}
}
if (!(json instanceof JSONObject)) {
return (T) json;
}
JSONObject jsonObj = (JSONObject) json;
if (info == LineNumberInfo.MissingInfo) {
info = jsonObj.getLineNumberInfo();
}
String classField = classInfo.getClassField();
String stype = jsonObj.optString(classField, null);
if ((stype == null) && Modifier.isAbstract(type.getModifiers()) &&
(jsonObj.length() == 1)) {
// if otherwise doomed to fail, try supporting "type-value : {...}" syntax
stype = jsonObj.keySet().iterator().next();
jsonObj = jsonObj.getJSONObject(stype);
}
try {
if (stype != null) {
Class> atype = classInfo.getClass(stype);
classInfo = Fields.getClassFieldMap(atype);
type = (Class) atype;
jsonObj.remove(classField);
}
} catch (ClassNotFoundException ex) {
String helpMessage = Plugins.classNameSuggestions(
PluginRegistry.defaultRegistry(), classInfo.getPluginMap(), stype);
throw new CodecExceptionLineNumber(new ClassNotFoundException(helpMessage, ex),
jsonObj.getValLineNumber(classField));
} catch (Exception ex) {
throw new CodecExceptionLineNumber(ex, jsonObj.getValLineNumber(classField));
}
try {
return decodeJSONInternal(classInfo, type.newInstance(), jsonObj, warnings);
} catch (InstantiationException ex) {
CodecExceptionLineNumber celn = translateInstantiationException(type, info);
throw celn;
} catch (IllegalAccessException ex) {
throw new CodecExceptionLineNumber("Could not access either the type or the constructor of " +
type.getName(), info);
}
}
private static CodecExceptionLineNumber translateInstantiationException(Class type, LineNumberInfo info) {
if (type.isInterface()) {
String msg = "Perhaps you failed to specify what type of object to create. " +
"Could not instantiate the interface " + type.getName() + ". ";
return new CodecExceptionLineNumber(msg, info);
} else if (Modifier.isAbstract(type.getModifiers())) {
String msg = "Perhaps you failed to specify what type of object to create. " +
"Could not instantiate the abstract class " + type.getName() + ". ";
return new CodecExceptionLineNumber(msg, info);
} else {
String msg = "Could not instantiate an instance of " + type.getName();
return new CodecExceptionLineNumber(msg, info);
}
}
@SuppressWarnings("unchecked")
public static T decodeJSON(T object, JSONObject json) throws CodecExceptionLineNumber, JSONException {
return decodeJSONInternal(Fields.getClassFieldMap(object.getClass()), object, json, null);
}
@SuppressWarnings("unchecked")
public static T decodeJSON(CodableClassInfo classInfo, T object, JSONObject json)
throws CodecExceptionLineNumber, JSONException {
return decodeJSONInternal(classInfo, object, json, null);
}
private static T decodeJSONInternal(CodableClassInfo classInfo, T object,
JSONObject json, List warnings)
throws CodecExceptionLineNumber, JSONException {
Config fieldDefaults = classInfo.getFieldDefaults();
if (object instanceof JSONCodable) {
try {
for (String key : fieldDefaults.root().keySet()) {
if (!json.has(key)) {
json.put(key, JSONObject.wrap(fieldDefaults.root().get(key).unwrapped()));
}
}
((JSONCodable) object).fromJSONObject(json);
} catch (Exception ex) {
throw new CodecExceptionLineNumber(ex, json.getLineNumberInfo());
}
return object;
}
Set unknownFields = new HashSet(json.keySet());
for (CodableFieldInfo field : classInfo.values()) {
if (field.isWriteOnly()) {
continue;
}
String fieldName = field.getName();
unknownFields.remove(fieldName);
Class type = field.getTypeOrComponentType();
Object value = json.opt(fieldName);
if ((value == null) && fieldDefaults.root().containsKey(fieldName)) {
value = CodecConfig.getDefault().hydrateField(field, fieldDefaults, object);
if (value != null) {
field.setStrictJson(object, json.getLineNumberInfo(), value, json.getValLineNumber(fieldName));
continue;
}
}
if (value == null) {
field.setStrictJson(object, json.getLineNumberInfo(), value, LineNumberInfo.MissingInfo);
continue;
}
LineNumberInfo keyInfo = json.getKeyLineNumber(fieldName);
LineNumberInfo valInfo = json.getValLineNumber(fieldName);
if (JSONCodable.class.isAssignableFrom(type)) {
Object oValue = value;
try {
value = type.newInstance();
} catch (InstantiationException ex) {
CodecExceptionLineNumber celn = translateInstantiationException(type, valInfo);
throw celn;
} catch (IllegalAccessException ex) {
throw new CodecExceptionLineNumber(
"Could not access the type or the constructor of " +
type.getName(), valInfo);
}
try {
((JSONCodable) value).fromJSONObject(new JSONObject(oValue.toString()));
} catch (Exception ex) {
throw new CodecExceptionLineNumber(ex, valInfo);
}
} else if (field.isArray()) {
value = decodeArrayInternal(type, value, valInfo, warnings);
} else if (field.isNative()) {
if (value.getClass() != type) {
if (value.getClass() == Integer.class || value.getClass() == int.class) {
// upconvert integer values to long if the field requires long
if (type == Long.class || type == long.class || type == AtomicLong.class) {
value = new Long(((Integer) value).longValue());
}
// upconvert integer values to double if the field requires double
if (type == Double.class || type == double.class) {
value = new Double(((Integer) value));
}
// downconvert integer to short if the field requires short
if (type == Short.class || type == short.class) {
value = new Short(((Integer) value).shortValue());
}
}
// downconvert double to float if the field requires a float
if ((value.getClass() == double.class || value.getClass() == Double.class) &&
(type == float.class || type == Float.class)) {
value = new Float(((Double) value));
}
// upconvert long values to double if the field requires double
if ((value.getClass() == long.class || value.getClass() == Long.class) &&
(type == double.class || type == Double.class)) {
value = new Double(((Integer) value));
}
// upconvert float values to double if the field requires double
if ((value.getClass() == float.class || value.getClass() == Float.class) &&
(type == double.class || type == Double.class)) {
value = new Double(((Float) value));
}
if (value.getClass() == String.class) {
try {
// convert String values to int if the field requires int
if (type == Integer.class || type == int.class ||
type == AtomicInteger.class) {
value = Integer.parseInt((String) value);
}
// convert String values to long if the field requires long
if (type == long.class || type == Long.class ||
type == AtomicLong.class) {
value = Long.parseLong((String) value);
}
// convert String values to double if the field requires double
if (type == double.class || type == Double.class) {
value = Double.parseDouble((String) value);
}
// convert String values to boolean if the field requires boolean
if (type == boolean.class || type == Boolean.class ||
type == AtomicBoolean.class) {
value = Boolean.parseBoolean((String) value);
}
} catch (NumberFormatException ex) {
if (type == Integer.class || type == int.class ||
type == AtomicInteger.class) {
throw new CodecExceptionLineNumber(
"cannot convert the string to an integer", valInfo);
} else if (type == long.class || type == Long.class ||
type == AtomicLong.class) {
throw new CodecExceptionLineNumber(
"cannot convert the string to a long", valInfo);
} else if (type == double.class || type == Double.class) {
throw new CodecExceptionLineNumber(
"cannot convert the string to a double", valInfo);
} else {
throw new IllegalStateException(
"unhandled case in the NumberFormatException");
}
}
}
if (type == AtomicInteger.class) {
value = new AtomicInteger((Integer) value);
} else if (type == AtomicLong.class) {
value = new AtomicLong((Long) value);
} else if (type == AtomicBoolean.class) {
value = new AtomicBoolean((Boolean) value);
}
}
// this space left intentionally blank
} else if (field.isMap()) {
Map map;
try {
map = (Map) type.newInstance();
} catch (Exception ex) {
throw new CodecExceptionLineNumber(ex, keyInfo);
}
JSONObject jmap = (JSONObject) value;
Class vc = (Class) field.getGenericTypes()[1];
boolean va = field.isMapValueArray();
for (Iterator iter = jmap.keys(); iter.hasNext(); ) {
String key = iter.next();
if (field.isInterned()) {
key = key.intern();
}
map.put(key, va ?
decodeArrayInternal(vc, jmap.get(key), jmap.getKeyLineNumber(key),
warnings)
:
decodeObjectInternal(vc, jmap.get(key), jmap.getKeyLineNumber(key),
warnings));
}
value = map;
} else if (field.isCollection()) {
Collection col;
JSONArray jarr;
try {
col = (Collection) type.newInstance();
} catch (Exception ex) {
throw new CodecExceptionLineNumber(ex, keyInfo);
}
try {
jarr = (JSONArray) value;
} catch (Exception ex) {
throw new CodecExceptionLineNumber(ex, valInfo);
}
Class vc = field.getCollectionClass();
boolean ar = field.isCollectionArray();
for (int i = 0; i < jarr.length(); i++) {
col.add(ar ?
decodeArrayInternal(vc, jarr.get(i), jarr.getLineNumber(i), warnings) :
decodeObjectInternal(vc, jarr.get(i), jarr.getLineNumber(i), warnings));
}
value = col;
} else if (field.isEnum()) {
try {
String valString = value.toString();
if (valString != "") {
value = Enum.valueOf(type, valString.toUpperCase());
}
} catch (Exception ex) {
throw new CodecExceptionLineNumber(ex, valInfo);
}
} else if (field.isCodable()) {
value = decodeObjectInternal(type, value, valInfo, warnings);
}
field.setStrictJson(object, json.getLineNumberInfo(), value, valInfo);
}
if (object instanceof SuperCodable) {
((SuperCodable) object).postDecode();
}
if (warnings != null) {
for (String fieldName : unknownFields) {
String msg = "Unrecognized field '" + fieldName + "' in class " + object.getClass().getName();
warnings.add(new UnrecognizedFieldException(msg, json.getKeyLineNumber(fieldName),
fieldName, object.getClass()));
}
}
return object;
}
}