/*
* Copyright 2012 Artur Keska.
*
* 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.jaxygen.converters.properties;
import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.commons.beanutils.ConvertUtils;
import org.apache.commons.beanutils.Converter;
import org.apache.commons.beanutils.converters.BooleanConverter;
import org.apache.commons.beanutils.converters.ByteConverter;
import org.apache.commons.beanutils.converters.CharacterConverter;
import org.apache.commons.beanutils.converters.DoubleConverter;
import org.apache.commons.beanutils.converters.FloatConverter;
import org.apache.commons.beanutils.converters.IntegerConverter;
import org.apache.commons.beanutils.converters.LongConverter;
import org.apache.commons.beanutils.converters.ShortConverter;
import org.apache.commons.beanutils.converters.StringConverter;
import org.jaxygen.converters.RequestConverter;
import org.jaxygen.converters.exceptions.DeserialisationError;
import org.jaxygen.dto.Uploadable;
import org.jaxygen.exceptions.WrongProperyIndex;
import org.jaxygen.http.HttpRequestParams;
import org.jaxygen.util.ClassTypeUtil;
/**
*
* @author Artur Keska
*/
public class PropertiesToBeanConverter implements RequestConverter {
static final Map<Class<?>, Converter> converters = new HashMap<Class<?>, Converter>();
static {
converters.put(Boolean.class, new BooleanConverter());
converters.put(Boolean.TYPE, new BooleanConverter());
converters.put(Byte.class, new ByteConverter());
converters.put(Byte.TYPE, new ByteConverter());
converters.put(Character.class, new CharacterConverter());
converters.put(Character.TYPE, new CharacterConverter());
converters.put(Float.class, new FloatConverter());
converters.put(Float.TYPE, new FloatConverter());
converters.put(Double.class, new DoubleConverter());
converters.put(Double.TYPE, new DoubleConverter());
converters.put(double.class, new DoubleConverter());
converters.put(Integer.class, new IntegerConverter());
converters.put(Integer.TYPE, new IntegerConverter());
converters.put(Long.class, new LongConverter());
converters.put(Long.TYPE, new LongConverter());
converters.put(Short.class, new ShortConverter());
converters.put(Short.TYPE, new ShortConverter());
converters.put(Enum.class, new EnumConverter());
converters.put(String.class, new StringConverter());
for (Class<?> c : converters.keySet()) {
ConvertUtils.register(converters.get(c), c);
}
}
static public boolean isCovertable(Class<?> c) {
return converters.containsKey(c);
}
public static final String NAME = "PROPERTIES";
@Override
public Object deserialise(HttpRequestParams params, Class<?> beanClass) throws DeserialisationError {
try {
return convertPropertiesToBean(params.getParameters(), params.getFiles(), beanClass);
} catch (Exception ex) {
throw new DeserialisationError("Could not parse input parameters for beed class " + beanClass, ex);
}
}
/**
* Applies a collection of properties to a JavaBean. Converts String and
* String[] values to correct property types
*
* @param properties A map of the properties to set on the JavaBean
* @param files List of files.
* @param beanClass Bean class to be converted.
* @return A new object of beanClass.
* @throws InstantiationException .
* @throws InvocationTargetException .
* @throws IllegalAccessException .
* @throws IntrospectionException .
* @throws IllegalArgumentException .
* @throws WrongProperyIndex Exception thrown on property validation errors.
*/
public static Object convertPropertiesToBean(Map<String, String> properties,
Map<String, Uploadable> files,
Class<?> beanClass) throws IllegalArgumentException,
IntrospectionException, IllegalAccessException,
InvocationTargetException, InstantiationException, WrongProperyIndex, NoSuchFieldException {
Object bean = beanClass.newInstance();
final SortedSet<String> sortedKeys = new TreeSet(properties.keySet());
final List<String> innerMapKeysToSkip = new ArrayList<>();
for (final String key : sortedKeys) {
if (!innerMapKeysToSkip.contains(key)) {
if (key.contains("<key>")) {
bean = deserializeMapField(key, properties, innerMapKeysToSkip, bean, beanClass);
} else if (key.contains("<value>")) {
//this property was handled in '<key>' condition
} else {
final String value = properties.get(key);
bean = fillBeanValueByName(key, value, beanClass, bean);
}
}
}
for (final String key : files.keySet()) {
final Uploadable value = files.get(key);
bean = fillBeanValueByName(key, value, beanClass, bean);
}
return bean;
}
private static Object deserializeMapField(final String key, Map<String, String> properties, final List<String> innerMapKeysToSkip, Object bean, Class<?> beanClass) throws NoSuchFieldException, IntrospectionException, WrongProperyIndex, IllegalArgumentException, IllegalAccessException, InstantiationException, InvocationTargetException {
final String keyBase = key.replace("<key>", "");
final String keykey = key;
final String keyval = properties.get(keykey);
final String valkey = key.replace("<key>", "<value>");
Object valval = properties.get(valkey);
if (valval == null) { // seems that value is more complex than just string
int bracketIndex = keyBase.indexOf("[");
final String fieldName = keyBase.substring(0, bracketIndex);
Map<String, String> innerProperties = new HashMap<>();
for (Map.Entry<String, String> entry : properties.entrySet()) {
if (entry.getKey().startsWith(valkey)) {
innerMapKeysToSkip.add(entry.getKey());
final String k = entry.getKey().replace(valkey + ".", "");
final String v = entry.getValue().replace(valkey + ".", "");
innerProperties.put(k, v);
}
}
Class<?>[] keyValueTypes = ClassTypeUtil.retrieveMapTypes(bean.getClass(), fieldName);
Class<?> valueType = keyValueTypes[1];
valval = convertPropertiesToBean(innerProperties, new HashMap(), valueType);
}
bean = fillBeanValueForMapField(keyBase, keyval, valval, beanClass, bean);
return bean;
}
public static Object convertPropertiesToBean(Map<String, String> properties,
Map<String, Uploadable> files,
Object bean) throws IllegalArgumentException,
IntrospectionException, IllegalAccessException,
InvocationTargetException, InstantiationException, WrongProperyIndex, NoSuchFieldException {
Object pojo = bean;
for (final String key : properties.keySet()) {
final String value = properties.get(key);
pojo = fillBeanValueByName(key, value, bean.getClass(), pojo);
}
for (final String key : files.keySet()) {
final Uploadable value = files.get(key);
pojo = fillBeanValueByName(key, value, bean.getClass(), pojo);
}
return bean;
}
/**
* Fill the field in bean by the value pointed by the name. Name format
* name=<(KEY([N])?)+> where KEY bean property name, N index in table (if
* bean field is List of java array).
*
* @param name
* @param value
* @param beanClass
* @param baseBean
* @param conversionReport
* @return
* @throws IntrospectionException
* @throws IllegalArgumentException
* @throws IllegalAccessException
* @throws InvocationTargetException
* @throws InstantiationException
* @throws WrongProperyIndex
*/
private static Object fillBeanValueByName(final String name, Object value,
Class<?> beanClass, Object baseBean)
throws IntrospectionException, IllegalArgumentException,
IllegalAccessException, InvocationTargetException,
InstantiationException, WrongProperyIndex, NoSuchFieldException {
// parse name x.y[i].z[n].v
Object bean = baseBean;
if (bean == null) {
bean = beanClass.newInstance();
}
Class<?> c = beanClass;
BeanInfo beanInfo = Introspector.getBeanInfo(c, Object.class);
final String childName = name.substring(name.indexOf(".") + 1);
String path[] = name.split("\\.");
final String fieldName = path[0];
// parse arrays [n]
if (fieldName.endsWith("]")) {
int bracketStart = fieldName.indexOf("[");
int len = fieldName.length();
if (bracketStart > 0) {
fillBeanArrayField(name, value, bean, beanInfo, path, fieldName,
bracketStart, len);
} else {
throw new WrongProperyIndex(name);
}
} else {
// parse non arrays
for (PropertyDescriptor pd : beanInfo.getPropertyDescriptors()) {
if (pd.getName().equals(fieldName)) {
Method writter = pd.getWriteMethod();
Method reader = pd.getReadMethod();
if (writter != null && reader != null) {
Class<?> valueType = reader.getReturnType();
if (path.length == 1) {
Object valueObject = parsePropertyToValue(value, valueType);
writter.invoke(bean, valueObject);
} else {
Object childBean = reader.invoke(bean);
Object valueObject = fillBeanValueByName(childName, value,
valueType, childBean);
writter.invoke(bean, valueObject);
}
}
}
}
}
// Object bean = c.newInstance();
return bean;
}
private static Object fillBeanValueForMapField(final String keybase,
Object keyval, Object valval,
Class<?> beanClass, Object baseBean)
throws IntrospectionException, IllegalArgumentException,
IllegalAccessException, InvocationTargetException,
InstantiationException, WrongProperyIndex, NoSuchFieldException {
// parse name x.y[i].z[n].v
Object bean = baseBean;
if (bean == null) {
bean = beanClass.newInstance();
}
final String fieldName = keybase;
// parse arrays [n]
if (fieldName.endsWith("]")) {
int bracketStart = fieldName.indexOf("[");
int len = fieldName.length();
if (bracketStart > 0) {
fillBeanMapField(keyval, valval, bean, beanClass, fieldName,
bracketStart, len);
} else {
throw new WrongProperyIndex(keybase);
}
}
return bean;
}
private static Class<?> retrieveListType(Class<?> paramClass, String propertyName) {
Class c = paramClass;
Field listField = null;
String name = c.getName();
while (listField == null || "java.lang.Object".equals(name)) {
try {
listField = c.getDeclaredField(propertyName);
} catch (Exception e) {
c = c.getSuperclass();
}
}
Type genericPropertyType = listField.getGenericType();
ParameterizedType propertyType = null;
while (propertyType == null) {
if ((genericPropertyType instanceof ParameterizedType)) {
propertyType = (ParameterizedType) genericPropertyType;
} else {
genericPropertyType = ((Class<?>) genericPropertyType).getGenericSuperclass();
}
}
return (Class<?>) propertyType.getActualTypeArguments()[0];
}
private static void fillBeanMapField(Object keyval, Object valval,
Object bean, Class<?> beanClass, final String fieldName,
int bracketStart, int len)
throws IllegalAccessException, InvocationTargetException,
IntrospectionException, InstantiationException, IllegalArgumentException,
WrongProperyIndex, NoSuchFieldException {
final String indexStr = fieldName.substring(bracketStart + 1, len - 1);
final String propertyName = fieldName.substring(0, bracketStart);
int index = Integer.parseInt(indexStr);
BeanInfo beanInfo = Introspector.getBeanInfo(beanClass, Object.class);
for (PropertyDescriptor pd : beanInfo.getPropertyDescriptors()) {
if (pd.getName().equals(propertyName)) {
Method writter = pd.getWriteMethod();
Method reader = pd.getReadMethod();
if (writter != null && reader != null) {
Object object = reader.invoke(bean);
if (pd.getPropertyType().isAssignableFrom(HashMap.class)) {
if (object == null) {
Class childType = pd.getPropertyType().getComponentType();
object = childType.newInstance();
writter.invoke(bean, object);
}
Class<?>[] keyValueTypes = ClassTypeUtil.retrieveMapTypes(bean.getClass(), propertyName);
Class<?> keyType = keyValueTypes[0];
Class<?> valueType = keyValueTypes[1];
Map map = (Map) object;
while (map.size() < (index + 1)) {
Object keyObject = parsePropertyToValue(keyval, keyType);
Object valueObject = parsePropertyToValue(valval, valueType);
map.put(keyObject, valueObject);
}
}
}
}
}
}
private static void fillBeanArrayField(final String name, Object value,
Object bean, BeanInfo beanInfo, String[] path, final String fieldName,
int bracketStart, int len)
throws IllegalAccessException, InvocationTargetException,
IntrospectionException, InstantiationException, IllegalArgumentException,
WrongProperyIndex, NoSuchFieldException {
final String indexStr = fieldName.substring(bracketStart + 1, len - 1);
final String propertyName = fieldName.substring(0, bracketStart);
int index = Integer.parseInt(indexStr);
String childName = "";
int firstDot = name.indexOf(".");
if (firstDot > 0) {
childName = name.substring(firstDot + 1);
}
for (PropertyDescriptor pd : beanInfo.getPropertyDescriptors()) {
if (pd.getName().equals(propertyName)) {
Method writter = pd.getWriteMethod();
Method reader = pd.getReadMethod();
if (writter != null && reader != null) {
Object array = reader.invoke(bean);
if (pd.getPropertyType().isAssignableFrom(ArrayList.class) || pd.getPropertyType().isAssignableFrom(LinkedList.class) || (List.class).isAssignableFrom(pd.getPropertyType())) { // List
if (array == null) {
Class childType = pd.getPropertyType().getComponentType();
array = childType.newInstance();
writter.invoke(bean, array);
}
Class<?> componentType = retrieveListType(bean.getClass(), propertyName);
List list = (List) array;
while (list.size() < (index + 1)) {
try {
list.add(componentType.getConstructor().newInstance());
} catch (NoSuchMethodException | SecurityException ex) {
Logger.getLogger(PropertiesToBeanConverter.class.getName()).log(Level.SEVERE, null, ex);
}
}
if (path.length == 1) {
Object valueObject = parsePropertyToValue(value, componentType);
list.set(index, valueObject);
} else {
Object valueObject = fillBeanValueByName(childName, value, componentType, list.get(index));
list.set(index, valueObject);
}
} else if (pd.getPropertyType().isArray()) {
if (array == null) {
array = Array.newInstance(
pd.getPropertyType().getComponentType(), index + 1);
writter.invoke(bean, array);
}
if (Array.getLength(array) < (index + 1)) {
array = resizeArray(array, index + 1);
writter.invoke(bean, array);
}
if (path.length == 1) {
Object valueObject = parsePropertyToValue(value, array.getClass().getComponentType());
Array.set(array, index, valueObject);
} else {
Object valueObject = fillBeanValueByName(childName, value, array.getClass().getComponentType(), Array.get(array, index));
Array.set(array, index, valueObject);
}
} else if (pd.getPropertyType().equals(List.class)) {
if (array == null) {
array = pd.getPropertyType().newInstance();
writter.invoke(bean, array);
}
Class<?> genericClass = array.getClass().getTypeParameters()[0].getClass();
if (path.length == 1) {
Object valueObject = parsePropertyToValue(value, genericClass);
Array.set(array, index, valueObject);
} else {
Object valueObject = fillBeanValueByName(childName, value,
genericClass, null);
Array.set(array, index, valueObject);
}
}
}
}
}
}
private static Object parsePropertyToValue(Object valueObject,
Class<?> propertyType) {
Object value = null;
//TODO: add cache of enum converters
boolean isEnum = propertyType.isEnum();
if (isEnum) {
ConvertUtils.register(new EnumConverter(), propertyType);
}
if (valueObject != null && valueObject.getClass().equals(String.class)) {
value = ConvertUtils.convert((String) valueObject, propertyType);
} else {
value = valueObject;
}
return value;
}
private static Object resizeArray(Object array, int size) {
Object newArray = Array.newInstance(array.getClass().getComponentType(),
size);
for (int i = 0; i < Array.getLength(array); i++) {
Object value = Array.get(array, i);
Array.set(newArray, i, value);
}
return newArray;
}
public String getName() {
return NAME;
}
}