package er.ajax.json.serializer;
import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.MethodDescriptor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import org.jabsorb.serializer.AbstractSerializer;
import org.jabsorb.serializer.MarshallException;
import org.jabsorb.serializer.ObjectMatch;
import org.jabsorb.serializer.SerializerState;
import org.jabsorb.serializer.UnmarshallException;
import org.json.JSONException;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import er.extensions.foundation.ERXStringUtilities;
/**
* ERXBeanSerializer is a rip-off of BeanSerializer except that it supports
* WO-style naming (i.e. missing "get").
*/
public class ERXBeanSerializer extends AbstractSerializer {
/**
* Stores the readable and writable properties for the Bean.
*/
protected static class BeanData {
// TODO: Legacy comment. WTF?
// in absence of getters and setters, these fields are
// public to allow subclasses to access.
/**
* The bean info for a certain bean
*/
public BeanInfo beanInfo;
/**
* The readable properties of the bean.
*/
public Map<String, Method> readableProps;
/**
* The writable properties of the bean.
*/
public Map<String, Method> writableProps;
}
/**
* Unique serialisation id.
*/
private final static long serialVersionUID = 2;
/**
* The logger for this class
*/
private final static Logger log = LoggerFactory.getLogger(ERXBeanSerializer.class);
/**
* Caches analysed beans
*/
private static Map<Class<?>, BeanData> beanCache = new HashMap<Class<?>, BeanData>();
/**
* Classes that this can serialise to.
*
* TODO: Yay for bloat!
*/
private static Class<?>[] _JSONClasses = new Class[] {};
private static Set<String> _ignoreMethodNames = new HashSet<>();
static {
_ignoreMethodNames = new HashSet<>();
_ignoreMethodNames.add("equals");
_ignoreMethodNames.add("hashCode");
_ignoreMethodNames.add("main");
_ignoreMethodNames.add("declaringClass");
}
private Class _clazz;
/**
* Analyses a bean, returning a BeanData with the data extracted from it.
*
* @param clazz The class of the bean to analyse
* @return A populated BeanData
* @throws IntrospectionException If a problem occurs during getting the bean
* info.
*/
public static BeanData analyzeBean(Class<?> clazz) throws IntrospectionException {
log.info("analyzing " + clazz.getName());
BeanData bd = new BeanData();
bd.beanInfo = Introspector.getBeanInfo(clazz, Object.class);
bd.readableProps = new HashMap<>();
bd.writableProps = new HashMap<>();
for (MethodDescriptor md : bd.beanInfo.getMethodDescriptors()) {
Method method = md.getMethod();
if (!Modifier.isStatic(method.getModifiers())) {
String name = method.getName();
if (!_ignoreMethodNames.contains(name)) {
Class<?> returnType = method.getReturnType();
String propertyName = name;
if (propertyName.startsWith("get") || propertyName.startsWith("set")) {
propertyName = ERXStringUtilities.uncapitalize(propertyName.substring("set".length()));
}
if (returnType == void.class && name.startsWith("set") && method.getParameterTypes().length == 1) {
bd.writableProps.put(propertyName, method);
}
else if (returnType != null && method.getParameterTypes().length == 0 && !name.startsWith("_")) {
bd.readableProps.put(propertyName, method);
}
}
}
}
return bd;
}
/**
* Gets the bean data from cache if possible, otherwise analyses the bean.
*
* @param clazz The class of the bean to analyse
* @return A populated BeanData
* @throws IntrospectionException If a problem occurs during getting the bean
* info.
*/
public static BeanData getBeanData(Class<?> clazz) throws IntrospectionException {
BeanData bd;
synchronized (beanCache) {
bd = beanCache.get(clazz);
if (bd == null) {
bd = analyzeBean(clazz);
beanCache.put(clazz, bd);
}
}
return bd;
}
public ERXBeanSerializer(Class clazz) {
_clazz = clazz;
}
@Override
public boolean canSerialize(Class clazz, Class jsonClazz) {
return (_clazz.isAssignableFrom(clazz) && (jsonClazz == null || jsonClazz == JSONObject.class));
}
public Class<?>[] getJSONClasses() {
return _JSONClasses;
}
public Class<?>[] getSerializableClasses() {
return new Class[] { _clazz };
}
public Object marshall(SerializerState state, Object p, Object o) throws MarshallException {
BeanData bd;
try {
bd = getBeanData(o.getClass());
}
catch (IntrospectionException e) {
throw new MarshallException(o.getClass().getName() + " is not a bean", e);
}
JSONObject val = new JSONObject();
if (ser.getMarshallClassHints()) {
try {
val.put("javaClass", o.getClass().getName());
}
catch (JSONException e) {
throw new MarshallException("JSONException: " + e.getMessage(), e);
}
}
Object args[] = new Object[0];
Object result;
for (Map.Entry<String, Method> ent : bd.readableProps.entrySet()) {
String prop = ent.getKey();
Method getMethod = ent.getValue();
if (log.isDebugEnabled()) {
log.debug("invoking " + getMethod.getName() + "()");
}
try {
result = getMethod.invoke(o, args);
}
catch (Throwable e) {
if (e instanceof InvocationTargetException) {
e = ((InvocationTargetException) e).getTargetException();
}
throw new MarshallException("bean " + o.getClass().getName() + " can't invoke " + getMethod.getName() + ": " + e.getMessage(), e);
}
try {
if (result != null || ser.getMarshallNullAttributes()) {
try {
Object json = ser.marshall(state, o, result, prop);
val.put(prop, json);
}
catch (JSONException e) {
throw new MarshallException("JSONException: " + e.getMessage(), e);
}
}
}
catch (MarshallException e) {
throw new MarshallException("bean " + o.getClass().getName() + " " + e.getMessage(), e);
}
}
return val;
}
public ObjectMatch tryUnmarshall(SerializerState state, Class clazz, Object o) throws UnmarshallException {
JSONObject jso = (JSONObject) o;
BeanData bd;
try {
bd = getBeanData(clazz);
}
catch (IntrospectionException e) {
throw new UnmarshallException(clazz.getName() + " is not a bean", e);
}
int match = 0;
int mismatch = 0;
for (Map.Entry<String, Method> ent : bd.writableProps.entrySet()) {
String prop = ent.getKey();
if (jso.has(prop)) {
match++;
}
else {
mismatch++;
}
}
if (match == 0) {
throw new UnmarshallException("bean has no matches");
}
// create a concrete ObjectMatch that is always returned in order to satisfy circular reference requirements
ObjectMatch returnValue = new ObjectMatch(-1);
state.setSerialized(o, returnValue);
ObjectMatch m = null;
ObjectMatch tmp;
Iterator<String> i = jso.keys();
while (i.hasNext()) {
String field = i.next();
Method setMethod = bd.writableProps.get(field);
if (setMethod != null) {
try {
Class<?> param[] = setMethod.getParameterTypes();
if (param.length != 1) {
throw new UnmarshallException("bean " + clazz.getName() + " method " + setMethod.getName() + " does not have one arg");
}
tmp = ser.tryUnmarshall(state, param[0], jso.get(field));
if (tmp != null) {
if (m == null) {
m = tmp;
}
else {
m = m.max(tmp);
}
}
}
catch (UnmarshallException e) {
throw new UnmarshallException("bean " + clazz.getName() + " " + e.getMessage(), e);
}
catch (JSONException e) {
throw new UnmarshallException("bean " + clazz.getName() + " " + e.getMessage(), e);
}
}
else {
mismatch++;
}
}
if (m != null) {
returnValue.setMismatch(m.max(new ObjectMatch(mismatch)).getMismatch());
}
else {
returnValue.setMismatch(mismatch);
}
return returnValue;
}
public Object unmarshall(SerializerState state, Class clazz, Object o) throws UnmarshallException {
JSONObject jso = (JSONObject) o;
BeanData bd;
try {
bd = getBeanData(clazz);
}
catch (IntrospectionException e) {
throw new UnmarshallException(clazz.getName() + " is not a bean", e);
}
if (log.isDebugEnabled()) {
log.debug("instantiating " + clazz.getName());
}
Object instance;
try {
instance = clazz.newInstance();
}
catch (InstantiationException e) {
throw new UnmarshallException("could not instantiate bean of type " + clazz.getName() + ", make sure it has a no argument " + "constructor and that it is not an interface or " + "abstract class", e);
}
catch (IllegalAccessException e) {
throw new UnmarshallException("could not instantiate bean of type " + clazz.getName(), e);
}
catch (RuntimeException e) {
throw new UnmarshallException("could not instantiate bean of type " + clazz.getName(), e);
}
state.setSerialized(o, instance);
Object invokeArgs[] = new Object[1];
Object fieldVal;
Iterator<String> i = jso.keys();
while (i.hasNext()) {
String field = i.next();
Method setMethod = bd.writableProps.get(field);
if (setMethod != null) {
try {
Class<?> param[] = setMethod.getParameterTypes();
fieldVal = ser.unmarshall(state, param[0], jso.get(field));
}
catch (UnmarshallException e) {
throw new UnmarshallException("could not unmarshall field \"" + field + "\" of bean " + clazz.getName(), e);
}
catch (JSONException e) {
throw new UnmarshallException("could not unmarshall field \"" + field + "\" of bean " + clazz.getName(), e);
}
if (log.isDebugEnabled()) {
log.debug("invoking " + setMethod.getName() + "(" + fieldVal + ")");
}
invokeArgs[0] = fieldVal;
try {
setMethod.invoke(instance, invokeArgs);
}
catch (Throwable e) {
if (e instanceof InvocationTargetException) {
e = ((InvocationTargetException) e).getTargetException();
}
throw new UnmarshallException("bean " + clazz.getName() + "can't invoke " + setMethod.getName() + ": " + e.getMessage(), e);
}
}
}
return instance;
}
}