/*
* This is part of Geomajas, a GIS framework, http://www.geomajas.org/.
*
* Copyright 2008-2011 Geosparc nv, http://www.geosparc.com/, Belgium.
*
* The program is available in open source according to the GNU Affero
* General Public License. All contributions in this program are covered
* by the Geomajas Contributors License Agreement. For full licensing
* details, see LICENSE.txt in the project root.
*/
package org.geomajas.dojo.server.json;
import com.metaparadigm.jsonrpc.AbstractSerializer;
import com.metaparadigm.jsonrpc.MarshallException;
import com.metaparadigm.jsonrpc.ObjectMatch;
import com.metaparadigm.jsonrpc.SerializerState;
import com.metaparadigm.jsonrpc.UnmarshallException;
import org.geomajas.global.Json;
import org.geomajas.layer.feature.attribute.PrimitiveAttribute;
import org.geomajas.service.BeanNameSimplifier;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* Json serializer which considers the @Json annotation.
*
* @author Jan De Moerloose
*/
@Component("dojo.server.json.AnnotatedBeanSerializer")
public class AnnotatedBeanSerializer extends AbstractSerializer {
private static final long serialVersionUID = 1;
private final Logger log = LoggerFactory.getLogger(AnnotatedBeanSerializer.class);
@Autowired
private ApplicationContext applicationContext;
@Autowired
private BeanNameSimplifier beanNameSimplifier;
private static final Map<Class, BeanData> BEAN_CACHE = new ConcurrentHashMap<Class, BeanData>();
private static final Class[] SERIALIZABLE_CLASSES = new Class[] {};
private static final Class[] JSON_CLASSES = new Class[] {};
public Class[] getSerializableClasses() {
return SERIALIZABLE_CLASSES;
}
public Class[] getJSONClasses() {
return JSON_CLASSES;
}
public boolean canSerialize(Class clazz, Class jsonClazz) {
return (!clazz.isArray() && !clazz.isPrimitive() && (jsonClazz == null ||
jsonClazz == JSONObject.class));
}
/**
* ???
*/
private static class BeanData {
// in absence of getters and setters, these fields are
// public to allow subclasses to access.
private BeanInfo beanInfo;
private HashMap<String, Method> readableProps;
private HashMap<String, Method> writableProps;
public BeanInfo getBeanInfo() {
return beanInfo;
}
public void setBeanInfo(BeanInfo beanInfo) {
this.beanInfo = beanInfo;
}
public HashMap<String, Method> getReadableProps() {
return readableProps;
}
public void setReadableProps(HashMap<String, Method> readableProps) {
this.readableProps = readableProps;
}
public HashMap<String, Method> getWritableProps() {
return writableProps;
}
public void setWritableProps(HashMap<String, Method> writableProps) {
this.writableProps = writableProps;
}
}
/**
* Bean serializer state.
*/
public static class BeanSerializerState {
// in absence of getters and setters, these fields are
// public to allow subclasses to access.
// Circular reference detection
private HashSet beanSet = new HashSet();
public HashSet getBeanSet() {
return beanSet;
}
public void setBeanSet(HashSet beanSet) {
this.beanSet = beanSet;
}
}
private BeanData analyzeBean(Class clazz) throws IntrospectionException {
log.debug("analyzing {}", clazz.getName());
BeanData bd = new BeanData();
bd.beanInfo = Introspector.getBeanInfo(clazz, clazz.isEnum() ? Enum.class : Object.class);
bd.readableProps = new HashMap<String, Method>();
bd.writableProps = new HashMap<String, Method>();
PropertyDescriptor[] props = bd.beanInfo.getPropertyDescriptors();
for (PropertyDescriptor prop : props) {
Method writeMethod = prop.getWriteMethod();
if (writeMethod != null) {
Json json = writeMethod.getAnnotation(Json.class);
if (json == null || json.serialize()) {
bd.writableProps.put(prop.getName(), writeMethod);
} else {
log.debug("skipping property {} for {}", prop.getName(), clazz.getName());
}
}
Method readMethod = prop.getReadMethod();
// due to a bug in the JAXB spec, Boolean getters are using isXXX
// i.o. getXXX as
// expected by bean introspectors
if (readMethod == null) {
if (writeMethod != null && writeMethod.getParameterTypes()[0].equals(Boolean.class)) {
if (writeMethod.getName().startsWith("set")) {
String isMethodName = "is" + writeMethod.getName().substring(3);
try {
readMethod = clazz.getMethod(isMethodName);
if (!readMethod.getReturnType().equals(Boolean.class)) {
readMethod = null;
}
} catch (Exception e) {
log.error("discarding:" + e.getMessage());
}
}
}
}
if (readMethod != null) {
Json json = readMethod.getAnnotation(Json.class);
if (json == null || json.serialize()) {
bd.readableProps.put(prop.getName(), readMethod);
}
}
}
if (clazz.isEnum()) {
try {
bd.readableProps.put("value", clazz.getMethod("value"));
} catch (Exception e) {
try {
bd.readableProps.put("value", Enum.class.getMethod("name"));
} catch (Exception e1) {
log.warn("cannot extract value of enum " + clazz.getName());
}
}
}
return bd;
}
private BeanData getBeanData(Class clazz) throws IntrospectionException {
BeanData bd = BEAN_CACHE.get(clazz);
if (bd == null) {
bd = analyzeBean(clazz);
BEAN_CACHE.put(clazz, bd);
}
return bd;
}
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");
}
int match = 0, mismatch = 0;
Iterator i = bd.writableProps.entrySet().iterator();
while (i.hasNext()) {
Map.Entry ent = (Map.Entry) i.next();
String prop = (String) ent.getKey();
if (jso.has(prop)) {
match++;
} else {
mismatch++;
}
}
if (match == 0) {
throw new UnmarshallException("bean has no matches");
}
ObjectMatch m = null, tmp;
i = jso.keys();
while (i.hasNext()) {
String field = (String) 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 (m == null) {
m = tmp;
} else {
m = m.max(tmp);
}
} catch (UnmarshallException e) {
throw new UnmarshallException("bean " + clazz.getName() + " " + e.getMessage());
}
} else {
mismatch++;
}
}
return m.max(new ObjectMatch(mismatch));
}
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");
}
log.debug("instantiating {}", clazz.getName());
Object instance;
try {
String beanName = beanNameSimplifier.simplify(clazz.getName());
if (applicationContext.containsBean(beanName)) {
instance = applicationContext.getBean(beanName);
} else {
log.debug("instantiating " + clazz.getName());
instance = clazz.newInstance();
}
} catch (Exception e) {
log.error(e.getMessage(), e);
throw new UnmarshallException("can't instantiate bean " + clazz.getName() + ": " + e.getMessage());
}
Object[] invokeArgs = new Object[1];
Object fieldVal;
Iterator i = jso.keys();
while (i.hasNext()) {
String field = (String) i.next();
Method setMethod = bd.writableProps.get(field);
if (setMethod != null) {
try {
Class param = setMethod.getParameterTypes()[0];
if (instance instanceof PrimitiveAttribute && "value".equals(field)) {
log.debug("replace 'value' type for PrimitiveAttribute");
switch(((PrimitiveAttribute) instance).getType()) {
case BOOLEAN:
param = Boolean.class;
break;
case DATE:
param = Date.class;
break;
case DOUBLE:
param = Double.class;
break;
case FLOAT:
param = Float.class;
break;
case INTEGER:
param = Integer.class;
break;
case LONG:
param = Long.class;
break;
case SHORT:
param = Short.class;
break;
case CURRENCY:
case IMGURL:
case URL:
case STRING:
param = String.class;
break;
default:
throw new UnmarshallException("Unknown type of PrimitiveAttribute " +
((PrimitiveAttribute) instance).getType());
}
}
fieldVal = ser.unmarshall(state, param, jso.get(field));
} catch (UnmarshallException e) {
throw new UnmarshallException("bean " + clazz.getName() + " " + e.getMessage());
}
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());
}
}
}
return instance;
}
public Object marshall(SerializerState state, Object o) throws MarshallException {
BeanSerializerState beanState;
try {
beanState = (BeanSerializerState) state.get(BeanSerializerState.class);
} catch (Exception e) {
e.printStackTrace();
throw new MarshallException("bean serializer internal error : " + e.getMessage());
}
Integer identity = System.identityHashCode(o);
if (beanState.beanSet.contains(identity)) {
throw new MarshallException("circular reference");
}
beanState.beanSet.add(identity);
BeanData bd;
try {
bd = getBeanData(o.getClass());
} catch (IntrospectionException e) {
throw new MarshallException(o.getClass().getName() + " is not a bean");
}
JSONObject val = new JSONObject();
if (ser.getMarshallClassHints()) {
val.put("javaClass", o.getClass().getName());
}
Iterator i = bd.readableProps.entrySet().iterator();
Object[] args = new Object[0];
Object result;
while (i.hasNext()) {
Map.Entry ent = (Map.Entry) i.next();
String prop = (String) ent.getKey();
Method getMethod = (Method) ent.getValue();
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());
}
try {
if (result != null || ser.getMarshallNullAttributes()) {
val.put(prop, ser.marshall(state, result));
}
} catch (MarshallException e) {
throw new MarshallException("bean " + o.getClass().getName() + " " + e.getMessage());
}
}
beanState.beanSet.remove(identity);
return val;
}
}