blob: b1867ccd305f555ec334299f89a1bdbe6ff04ddd [file] [log] [blame]
//
// ========================================================================
// Copyright (c) 1995-2014 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.util.ajax;
import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import org.eclipse.jetty.util.ajax.JSON.Output;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
/**
* Converts POJOs to JSON and vice versa.
* The key difference:
* - returns the actual object from Convertor.fromJSON (JSONObjectConverter returns a Map)
* - the getters/setters are resolved at initialization (JSONObjectConverter resolves it at runtime)
* - correctly sets the number fields
*
*/
public class JSONPojoConvertor implements JSON.Convertor
{
private static final Logger LOG = Log.getLogger(JSONPojoConvertor.class);
public static final Object[] GETTER_ARG = new Object[]{}, NULL_ARG = new Object[]{null};
private static final Map<Class<?>, NumberType> __numberTypes = new HashMap<Class<?>, NumberType>();
public static NumberType getNumberType(Class<?> clazz)
{
return __numberTypes.get(clazz);
}
protected boolean _fromJSON;
protected Class<?> _pojoClass;
protected Map<String,Method> _getters = new HashMap<String,Method>();
protected Map<String,Setter> _setters = new HashMap<String,Setter>();
protected Set<String> _excluded;
/**
* @param pojoClass The class to convert
*/
public JSONPojoConvertor(Class<?> pojoClass)
{
this(pojoClass, (Set<String>)null, true);
}
/**
* @param pojoClass The class to convert
* @param excluded The fields to exclude
*/
public JSONPojoConvertor(Class<?> pojoClass, String[] excluded)
{
this(pojoClass, new HashSet<String>(Arrays.asList(excluded)), true);
}
/**
* @param pojoClass The class to convert
* @param excluded The fields to exclude
*/
public JSONPojoConvertor(Class<?> pojoClass, Set<String> excluded)
{
this(pojoClass, excluded, true);
}
/**
* @param pojoClass The class to convert
* @param excluded The fields to exclude
* @param fromJSON If true, add a class field to the JSON
*/
public JSONPojoConvertor(Class<?> pojoClass, Set<String> excluded, boolean fromJSON)
{
_pojoClass = pojoClass;
_excluded = excluded;
_fromJSON = fromJSON;
init();
}
/**
* @param pojoClass The class to convert
* @param fromJSON If true, add a class field to the JSON
*/
public JSONPojoConvertor(Class<?> pojoClass, boolean fromJSON)
{
this(pojoClass, (Set<String>)null, fromJSON);
}
/* ------------------------------------------------------------ */
protected void init()
{
Method[] methods = _pojoClass.getMethods();
for (int i=0;i<methods.length;i++)
{
Method m=methods[i];
if (!Modifier.isStatic(m.getModifiers()) && m.getDeclaringClass()!=Object.class)
{
String name=m.getName();
switch(m.getParameterTypes().length)
{
case 0:
if(m.getReturnType()!=null)
{
if (name.startsWith("is") && name.length()>2)
name=name.substring(2,3).toLowerCase(Locale.ENGLISH)+name.substring(3);
else if (name.startsWith("get") && name.length()>3)
name=name.substring(3,4).toLowerCase(Locale.ENGLISH)+name.substring(4);
else
break;
if(includeField(name, m))
addGetter(name, m);
}
break;
case 1:
if (name.startsWith("set") && name.length()>3)
{
name=name.substring(3,4).toLowerCase(Locale.ENGLISH)+name.substring(4);
if(includeField(name, m))
addSetter(name, m);
}
break;
}
}
}
}
/* ------------------------------------------------------------ */
protected void addGetter(String name, Method method)
{
_getters.put(name, method);
}
/* ------------------------------------------------------------ */
protected void addSetter(String name, Method method)
{
_setters.put(name, new Setter(name, method));
}
/* ------------------------------------------------------------ */
protected Setter getSetter(String name)
{
return _setters.get(name);
}
/* ------------------------------------------------------------ */
protected boolean includeField(String name, Method m)
{
return _excluded==null || !_excluded.contains(name);
}
/* ------------------------------------------------------------ */
protected int getExcludedCount()
{
return _excluded==null ? 0 : _excluded.size();
}
/* ------------------------------------------------------------ */
public Object fromJSON(Map object)
{
Object obj = null;
try
{
obj = _pojoClass.newInstance();
}
catch(Exception e)
{
// TODO return Map instead?
throw new RuntimeException(e);
}
setProps(obj, object);
return obj;
}
/* ------------------------------------------------------------ */
public int setProps(Object obj, Map<?,?> props)
{
int count = 0;
for(Iterator<?> iterator = props.entrySet().iterator(); iterator.hasNext();)
{
Map.Entry<?, ?> entry = (Map.Entry<?,?>) iterator.next();
Setter setter = getSetter((String)entry.getKey());
if(setter!=null)
{
try
{
setter.invoke(obj, entry.getValue());
count++;
}
catch(Exception e)
{
// TODO throw exception?
LOG.warn(_pojoClass.getName()+"#"+setter.getPropertyName()+" not set from "+
(entry.getValue().getClass().getName())+"="+entry.getValue().toString());
log(e);
}
}
}
return count;
}
/* ------------------------------------------------------------ */
public void toJSON(Object obj, Output out)
{
if(_fromJSON)
out.addClass(_pojoClass);
for(Map.Entry<String,Method> entry : _getters.entrySet())
{
try
{
out.add(entry.getKey(), entry.getValue().invoke(obj, GETTER_ARG));
}
catch(Exception e)
{
// TODO throw exception?
LOG.warn("{} property '{}' excluded. (errors)", _pojoClass.getName(),
entry.getKey());
log(e);
}
}
}
/* ------------------------------------------------------------ */
protected void log(Throwable t)
{
LOG.ignore(t);
}
/* ------------------------------------------------------------ */
public static class Setter
{
protected String _propertyName;
protected Method _setter;
protected NumberType _numberType;
protected Class<?> _type;
protected Class<?> _componentType;
public Setter(String propertyName, Method method)
{
_propertyName = propertyName;
_setter = method;
_type = method.getParameterTypes()[0];
_numberType = __numberTypes.get(_type);
if(_numberType==null && _type.isArray())
{
_componentType = _type.getComponentType();
_numberType = __numberTypes.get(_componentType);
}
}
public String getPropertyName()
{
return _propertyName;
}
public Method getMethod()
{
return _setter;
}
public NumberType getNumberType()
{
return _numberType;
}
public Class<?> getType()
{
return _type;
}
public Class<?> getComponentType()
{
return _componentType;
}
public boolean isPropertyNumber()
{
return _numberType!=null;
}
public void invoke(Object obj, Object value) throws IllegalArgumentException,
IllegalAccessException, InvocationTargetException
{
if(value==null)
_setter.invoke(obj, NULL_ARG);
else
invokeObject(obj, value);
}
protected void invokeObject(Object obj, Object value) throws IllegalArgumentException,
IllegalAccessException, InvocationTargetException
{
if (_type.isEnum())
{
if (value instanceof Enum)
_setter.invoke(obj, new Object[]{value});
else
_setter.invoke(obj, new Object[]{Enum.valueOf((Class<? extends Enum>)_type,value.toString())});
}
else if(_numberType!=null && value instanceof Number)
{
_setter.invoke(obj, new Object[]{_numberType.getActualValue((Number)value)});
}
else if (Character.TYPE.equals(_type) || Character.class.equals(_type))
{
_setter.invoke(obj, new Object[]{String.valueOf(value).charAt(0)});
}
else if(_componentType!=null && value.getClass().isArray())
{
if(_numberType==null)
{
int len = Array.getLength(value);
Object array = Array.newInstance(_componentType, len);
try
{
System.arraycopy(value, 0, array, 0, len);
}
catch(Exception e)
{
// unusual array with multiple types
LOG.ignore(e);
_setter.invoke(obj, new Object[]{value});
return;
}
_setter.invoke(obj, new Object[]{array});
}
else
{
Object[] old = (Object[])value;
Object array = Array.newInstance(_componentType, old.length);
try
{
for(int i=0; i<old.length; i++)
Array.set(array, i, _numberType.getActualValue((Number)old[i]));
}
catch(Exception e)
{
// unusual array with multiple types
LOG.ignore(e);
_setter.invoke(obj, new Object[]{value});
return;
}
_setter.invoke(obj, new Object[]{array});
}
}
else
_setter.invoke(obj, new Object[]{value});
}
}
public interface NumberType
{
public Object getActualValue(Number number);
}
public static final NumberType SHORT = new NumberType()
{
public Object getActualValue(Number number)
{
return new Short(number.shortValue());
}
};
public static final NumberType INTEGER = new NumberType()
{
public Object getActualValue(Number number)
{
return new Integer(number.intValue());
}
};
public static final NumberType FLOAT = new NumberType()
{
public Object getActualValue(Number number)
{
return new Float(number.floatValue());
}
};
public static final NumberType LONG = new NumberType()
{
public Object getActualValue(Number number)
{
return number instanceof Long ? number : new Long(number.longValue());
}
};
public static final NumberType DOUBLE = new NumberType()
{
public Object getActualValue(Number number)
{
return number instanceof Double ? number : new Double(number.doubleValue());
}
};
static
{
__numberTypes.put(Short.class, SHORT);
__numberTypes.put(Short.TYPE, SHORT);
__numberTypes.put(Integer.class, INTEGER);
__numberTypes.put(Integer.TYPE, INTEGER);
__numberTypes.put(Long.class, LONG);
__numberTypes.put(Long.TYPE, LONG);
__numberTypes.put(Float.class, FLOAT);
__numberTypes.put(Float.TYPE, FLOAT);
__numberTypes.put(Double.class, DOUBLE);
__numberTypes.put(Double.TYPE, DOUBLE);
}
}