/*
* Copyright 2011 Future Systems
*
* 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 org.krakenapps.api;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class PrimitiveConverter {
@SuppressWarnings("unchecked")
private static Set<Class<?>> nonSerializeClasses = new HashSet<Class<?>>(Arrays.asList(byte.class, Byte.class, short.class,
Short.class, int.class, Integer.class, long.class, Long.class, float.class, Float.class, double.class, Double.class,
boolean.class, Boolean.class, char.class, Character.class, String.class, Date.class));
public static enum SerializeOption {
INCLUDE_SKIP_FIELD
}
public static Object serialize(Object obj) {
return serialize(obj, obj, null, null);
}
public static Object serialize(Object obj, PrimitiveSerializeCallback callback) {
return serialize(obj, obj, callback, null);
}
public static Object serialize(Object obj, SerializeOption... options) {
return serialize(obj, obj, null, null, options);
}
public static Object serialize(Object obj, PrimitiveSerializeCallback callback, SerializeOption... options) {
return serialize(obj, obj, callback, null, options);
}
@SuppressWarnings("unchecked")
private static Object serialize(Object root, Object obj, PrimitiveSerializeCallback callback, List<String> refkey,
SerializeOption... options) {
if (obj == null)
return null;
List<SerializeOption> optionList = Arrays.asList(options);
Class<?> cls = obj.getClass();
// Primitive Object
if (nonSerializeClasses.contains(cls))
return obj;
// Array
if (cls.isArray()) {
if (cls.getComponentType().isPrimitive())
return obj;
Object[] ary = new Object[Array.getLength(obj)];
for (int i = 0; i < ary.length; i++)
ary[i] = serialize(root, Array.get(obj, i), callback, refkey, options);
return ary;
}
// Collection
if (Collection.class.isAssignableFrom(cls)) {
Collection<Object> col = new ArrayList<Object>();
for (Object o : (Collection<Object>) obj)
col.add(serialize(root, o, callback, refkey, options));
return col;
}
// Map
if (Map.class.isAssignableFrom(cls)) {
Map<Object, Object> m = new HashMap<Object, Object>();
Map<Object, Object> o = (Map<Object, Object>) obj;
for (Object key : o.keySet()) {
Object k = serialize(root, key, callback, refkey, options);
Object value = o.get(key);
Object v = serialize(root, value, callback, refkey, options);
m.put(k, v);
}
return m;
}
// Others
Map<String, Object> m = new HashMap<String, Object>();
for (Field f : cls.getDeclaredFields()) {
// skip static field
if (Modifier.isStatic(f.getModifiers()))
continue;
try {
FieldOption option = f.getAnnotation(FieldOption.class);
if (option != null && option.skip() && !optionList.contains(SerializeOption.INCLUDE_SKIP_FIELD))
continue;
if (refkey != null && !refkey.contains(f.getName()))
continue;
String fieldName = toUnderscoreName(f.getName());
if (option != null && !option.name().isEmpty())
fieldName = option.name();
f.setAccessible(true);
Object value = f.get(obj);
if (option != null && !option.nullable() && value == null)
throw new IllegalArgumentException(String.format("Can not set %s field %s.%s to null value", f.getType()
.getSimpleName(), cls.getName(), f.getName()));
if (value instanceof Enum) {
m.put(fieldName, value.toString());
} else if (value instanceof String) {
if (option != null && option.length() > 0 && ((String) value).length() > option.length()) {
String s = (String) value;
throw new IllegalArgumentException(String.format(
"Too long String value for %s.%s (limit: %d, input: [%s])", cls.getName(), f.getName(),
option.length(), s));
}
m.put(fieldName, value);
} else {
if (value == null)
m.put(fieldName, null);
else {
List<String> referenceKey = null;
if (f.getAnnotation(ReferenceKey.class) != null && callback != null)
referenceKey = Arrays.asList(f.getAnnotation(ReferenceKey.class).value());
Object serialized = serialize(root, value, callback, referenceKey, options);
m.put(fieldName, serialized);
}
}
} catch (Exception e) {
throw new RuntimeException("kraken api: serialize failed", e);
}
}
if (callback != null && refkey != null && !(refkey.containsAll(m.keySet()) && m.keySet().containsAll(refkey)))
callback.onSerialize(root, cls, obj, m);
return m;
}
public static <T> T parse(Class<T> cls, Object obj) {
return parse(cls, obj, null);
}
@SuppressWarnings({ "unchecked", "rawtypes" })
public static <T> T parse(Class<T> cls, Object obj, PrimitiveParseCallback callback) {
if (obj == null || !(obj instanceof Map) || cls.equals(Object.class))
return (T) obj;
try {
Map<String, Object> m = (Map<String, Object>) obj;
T n = null;
try {
n = cls.newInstance();
} catch (IllegalAccessException e) {
Constructor c = cls.getDeclaredConstructor();
c.setAccessible(true);
n = (T) c.newInstance();
}
for (Field f : cls.getDeclaredFields()) {
FieldOption option = f.getAnnotation(FieldOption.class);
if (option != null && option.skip())
continue;
String fieldName = toUnderscoreName(f.getName());
if (option != null && !option.name().isEmpty())
fieldName = option.name();
if (!m.containsKey(fieldName))
continue;
Object value = m.get(fieldName);
f.setAccessible(true);
if (value == null) {
f.set(n, null);
continue;
}
Class<?> fieldType = f.getType();
ReferenceKey refkey = f.getAnnotation(ReferenceKey.class);
if (refkey != null) {
if (value instanceof Map) {
Map<String, Object> keys = (Map<String, Object>) value;
if (callback != null)
f.set(n, callback.onParse(fieldType, getRefKeys(keys, refkey.value())));
else if (option != null && !option.nullable())
throw new IllegalArgumentException(fieldName + " requires parse callback");
} else if (value instanceof Object[]) {
List<Object> coll = new ArrayList<Object>();
for (Object v : (Object[]) value) {
Map<String, Object> keys = (Map<String, Object>) v;
if (callback != null) {
Object o = callback.onParse(f.getAnnotation(CollectionTypeHint.class).value(),
getRefKeys(keys, refkey.value()));
if (o != null)
coll.add(o);
} else if (option != null && !option.nullable())
throw new IllegalArgumentException(fieldName + " requires parse callback");
}
// TODO
if (List.class.isAssignableFrom(fieldType))
f.set(n, coll);
else if (Set.class.isAssignableFrom(fieldType))
f.set(n, new HashSet<Object>(coll));
else if (fieldType.isArray())
f.set(n, coll.toArray());
}
continue;
}
if (fieldType.isEnum()) {
Object found = null;
for (Object o : fieldType.getEnumConstants())
if (o.toString().equals(value))
found = o;
f.set(n, found);
} else if (fieldType.isArray()) {
if (fieldType.getComponentType().isPrimitive())
f.set(n, value);
else {
Object[] o = (Object[]) value;
Object ary = Array.newInstance(fieldType.getComponentType(), o.length);
for (int i = 0; i < o.length; i++)
Array.set(ary, i, o[i]);
f.set(n, ary);
}
} else if (Collection.class.isAssignableFrom(fieldType)) {
if (value instanceof Object[])
value = Arrays.asList((Object[]) value);
CollectionTypeHint hint = f.getAnnotation(CollectionTypeHint.class);
if (value instanceof List) {
if (hint != null)
f.set(n, parseCollection(hint.value(), (List) value));
else
f.set(n, value);
}
} else if (Map.class.isAssignableFrom(fieldType)) {
MapTypeHint hint = f.getAnnotation(MapTypeHint.class);
if (hint == null) {
f.set(n, value);
} else {
Map<Object, Object> v = (Map<Object, Object>) value;
Map<Object, Object> fmap = new HashMap<Object, Object>();
for (Object k : v.keySet())
fmap.put(parse(hint.value()[0], k, callback), parse(hint.value()[1], v.get(k), callback));
f.set(n, fmap);
}
} else {
f.set(n, parse(fieldType, value, callback));
}
}
return n;
} catch (InstantiationException e) {
throw new RuntimeException("Primitive parse failed. Please check if nullary constructor is accessible", e);
} catch (Exception e) {
throw new RuntimeException("Primitive parse failed", e);
}
}
private static Map<String, Object> getRefKeys(Map<String, Object> src, String[] refkeys) {
Map<String, Object> m = new HashMap<String, Object>();
for (String key : refkeys) {
key = toUnderscoreName(key);
m.put(key, src.get(key));
}
return m;
}
public static <T> Collection<T> parseCollection(Class<T> cls, Collection<Object> coll) {
return parseCollection(cls, coll, null);
}
@SuppressWarnings("unchecked")
public static <T> Collection<T> parseCollection(Class<T> cls, Collection<Object> coll, PrimitiveParseCallback callback) {
Collection<T> result = new ArrayList<T>();
for (Object obj : coll) {
if (Map.class.isAssignableFrom(obj.getClass()))
result.add(parse(cls, obj, callback));
else
result.add((T) obj);
}
return result;
}
public static Object overwrite(Object obj, Map<String, Object> m) {
return overwrite(obj, m, (PrimitiveParseCallback) null);
}
public static Object overwrite(Object obj, Map<String, Object> m, PrimitiveParseCallback callback) {
Object newObj = parse(obj.getClass(), m, callback);
return overwrite(obj, newObj, m);
}
@SuppressWarnings("unchecked")
private static Object overwrite(Object before, Object after, Map<String, Object> m) {
if (before == null || after == null)
return after;
try {
for (Field f : before.getClass().getDeclaredFields()) {
String fieldName = toUnderscoreName(f.getName());
if (m.containsKey(fieldName)) {
f.setAccessible(true);
Class<?> cls = f.getType();
Object newValue = m.get(fieldName);
if (newValue != null && Map.class.isAssignableFrom(newValue.getClass()) && !Map.class.isAssignableFrom(cls))
f.set(before, overwrite(f.get(before), f.get(after), (Map<String, Object>) newValue));
else
f.set(before, f.get(after));
}
}
} catch (Exception e) {
throw new RuntimeException("Primitive overwrite failed", e);
}
return before;
}
public static String toUnderscoreName(String s) {
final int tolower = 'a' - 'A';
StringBuilder sb = new StringBuilder(s.length() * 2);
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (c >= 'A' && c <= 'Z') {
if (i != 0)
sb.append("_");
sb.append((char) (c + tolower));
} else
sb.append(c);
}
return sb.toString();
}
}