/* * Copyright (c) 2015 EMC Corporation * All Rights Reserved */ package com.emc.sa.engine.bind; import java.lang.reflect.Array; import java.lang.reflect.Field; import java.net.URI; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import org.apache.commons.beanutils.ConvertUtilsBean; import org.apache.commons.beanutils.converters.IntegerConverter; import org.apache.commons.lang.StringUtils; import org.apache.log4j.Logger; import com.emc.sa.util.TextUtils; /** * Utilities for binding parameters into request objects. * * @author Chris Dail */ public class BindingUtils { private static Logger log = Logger.getLogger(BindingUtils.class); private static ConvertUtilsBean convertUtilsBean; static { convertUtilsBean = new ConvertUtilsBean(); convertUtilsBean.register(new CommaSeparatedListConverter(), List.class); convertUtilsBean.register(new URIConverter(), URI.class); convertUtilsBean.register(new IntegerConverter(null), Integer.class); } public static void bind(Object target, Map<String, Object> parameters) { bind(target, new MapParameterAccess(parameters)); } public static void bind(Object target, ParameterAccess parameters) { for (Field field : getAllDeclaredFields(target.getClass())) { bindField(target, field, parameters); } } /** * Gets all declared fields on the given type and its super classes. * * @param type * the type. * @return the list of declared fields. */ private static List<Field> getAllDeclaredFields(Class<?> type) { List<Field> fields = new ArrayList<>(); Class<?> superClass = type; while (!superClass.equals(Object.class)) { fields.addAll(Arrays.asList(superClass.getDeclaredFields())); superClass = superClass.getSuperclass(); } return fields; } /** * Binds a parameter to the field if applicable. * * @param target * the target object. * @param field * the field to bind. * @param parameters * the available parameters. */ private static void bindField(Object target, Field field, ParameterAccess parameters) { if (field.isAnnotationPresent(Param.class)) { Param param = field.getAnnotation(Param.class); bindParam(param, target, field, parameters); } else if (field.isAnnotationPresent(Bindable.class)) { Bindable bindable = field.getAnnotation(Bindable.class); boolean simpleBinding = Void.class.equals(bindable.itemType()); if (simpleBinding) { Object newTarget = getBindTarget(target, field); bind(newTarget, parameters); } else { bindList(bindable.itemType(), target, field, parameters); } } } /** * Binds a given parameters onto the target object. * * @param param * the paramter annotation. * @param target * the target object. * @param field * the target field of the parameter. * @param parameters * the available parameters. */ private static void bindParam(Param param, Object target, Field field, ParameterAccess parameters) { String fieldName = field.getName(); Class<?> fieldType = field.getType(); // Use the field name if no value is specified String paramName = StringUtils.defaultIfBlank(param.value(), fieldName); Object value = parameters.get(paramName); if (value == null && param.required()) { String message = String.format("Required parameter '%s' is missing for field %s on %s", paramName, fieldName, field.getDeclaringClass().getName()); throw new BindingException(message); } else if (value != null) { if (log.isDebugEnabled()) { log.debug(String.format("Binding parameter '%s' to field '%s'", paramName, fieldName)); } try { field.setAccessible(true); field.set(target, convert(value, fieldType)); } catch (Exception e) { String message = String.format("Error binding parameter '%s' to field '%s'", paramName, fieldName); throw new BindingException(message, e); } } } /** * Gets a new target for binding to the field value. If the field is null, a * new instance is created and set as the field value. * * @param target * the target object. * @param field * the bind field. * @return the bind target. */ private static Object getBindTarget(Object target, Field field) { try { field.setAccessible(true); Object fieldValue = field.get(target); if (fieldValue == null) { fieldValue = field.getType().newInstance(); field.set(target, fieldValue); } return fieldValue; } catch (Exception e) { String message = String.format("Error getting bind target for field '%s'", field.getName()); throw new BindingException(message, e); } } @SuppressWarnings({ "unchecked", "rawtypes" }) private static Object convert(Object value, Class type) { if (value instanceof String && !(type.isArray() || type.isAssignableFrom(List.class))) { List<String> parsedValue = TextUtils.parseCSV((String) value); if (parsedValue.isEmpty()) { value = ""; } else if (parsedValue.size() == 1) { value = parsedValue.get(0); } else { String message = String.format( "Value '%s' produced %d values when parsed from CSV. Should be 1", value, parsedValue.size()); throw new BindingException(message); } } Object convertedValue = convertUtilsBean.convert(value, type); // Handle the general purpose enum conversion if (type.isEnum() && (value != null) && !type.isInstance(convertedValue)) { convertedValue = Enum.valueOf(type, value.toString()); } return convertedValue; } /** * Binds a list value into the given field. * * @param itemType * the item type of the list. * @param target * the target object. * @param field * the target field. * @param parameters * the input parameters. */ private static void bindList(Class<?> itemType, Object target, Field field, ParameterAccess parameters) { String fieldName = field.getName(); Class<?> fieldType = field.getType(); List<ParameterAccess> itemParameters = createItemParameters(itemType, parameters); Object array = Array.newInstance(itemType, itemParameters.size()); for (int i = 0; i < itemParameters.size(); i++) { Object itemValue = createAndBindItem(itemParameters.get(i), itemType); Array.set(array, i, itemValue); } Object targetValue = convertArray(array, fieldType, itemType); try { field.setAccessible(true); field.set(target, targetValue); } catch (Exception e) { String message = String.format("Error binding list to field '%s'", fieldName); throw new BindingException(message, e); } } /** * Converts the array to the appropriate type for binding. * * @param array * the array to convert. * @param fieldType * the type of the target field. * @param itemType * the item type of the array. * @return */ @SuppressWarnings({ "rawtypes", "unchecked" }) private static Object convertArray(Object array, Class<?> fieldType, Class<?> itemType) { int len = Array.getLength(array); // Convert to a list if (fieldType.equals(List.class)) { List list = new ArrayList(); for (int i = 0; i < len; i++) { list.add(Array.get(array, i)); } return list; } // Array of an assignment compatible type else if (fieldType.isArray() && fieldType.getComponentType().isAssignableFrom(itemType)) { // Check for exact type match if (itemType.equals(fieldType.getComponentType())) { return array; } Object targetArray = Array.newInstance(fieldType.getComponentType(), len); for (int i = 0; i < len; i++) { Array.set(targetArray, i, Array.get(array, i)); } return targetArray; } else { String message = String.format("Cannot convert array of %s to %s", itemType, fieldType); throw new BindingException(message); } } /** * Creates and binds the parameters to a new item. * * @param parameters * the parameters. * @param itemType * the item type. * @return the new item. */ private static Object createAndBindItem(ParameterAccess parameters, Class<?> itemType) { try { Object value = itemType.newInstance(); bind(value, parameters); return value; } catch (InstantiationException | IllegalAccessException e) { throw new BindingException("Failed to instantiate new instance of " + itemType, e); } } /** * Creates a list of parameters for each item based on its type. The * provided parameters are treated as comma-separated columns and converted * into matching rows for each item in the list. * * @param itemType * the type of each item, the {@link Param}s of the type determine * what parameters are used to convert into items. * @param parameters * the input parameters. * @return the list of item parameters. */ private static List<ParameterAccess> createItemParameters(Class<?> itemType, ParameterAccess parameters) { List<Map<String, Object>> rows = new ArrayList<>(); for (String name : getParameterNames(itemType)) { Object value = parameters.get(name); List<String> values = TextUtils.parseCSV(value != null ? value.toString() : null); for (int i = 0; i < values.size(); i++) { getOrCreateRow(rows, i).put(name, values.get(i)); } } // Converts the row maps into parameters List<ParameterAccess> results = new ArrayList<>(); for (Map<String, Object> row : rows) { results.add(new MapParameterAccess(row)); } return results; } /** * Gets the names of the parameters in a given type. * * @param type * the type. * @return the set of parameter names. */ private static Set<String> getParameterNames(Class<?> type) { Set<String> names = new LinkedHashSet<>(); for (Field field : getAllDeclaredFields(type)) { Param param = field.getAnnotation(Param.class); if (param != null) { names.add(StringUtils.defaultIfBlank(param.value(), field.getName())); } } return names; } private static Map<String, Object> getOrCreateRow(List<Map<String, Object>> rows, int index) { while (rows.size() <= index) { rows.add(new HashMap<String, Object>()); } return rows.get(index); } private static final class MapParameterAccess implements ParameterAccess { private Map<String, Object> data; public MapParameterAccess(Map<String, Object> data) { this.data = data; } @Override public Set<String> getNames() { return data.keySet(); } @Override public Object get(String name) { return data.get(name); } @Override public void set(String name, Object value) { data.put(name, value); } } }