/** * $Id: FieldUtils.java 129 2014-03-18 23:25:36Z azeckoski $ * $URL: http://reflectutils.googlecode.com/svn/trunk/src/main/java/org/azeckoski/reflectutils/FieldUtils.java $ * FieldUtils.java - genericdao - May 19, 2008 10:10:15 PM - azeckoski ************************************************************************** * Copyright (c) 2008 Aaron Zeckoski * Licensed under the Apache License, Version 2.0 * * A copy of the Apache License has been included in this * distribution and is available at: http://www.apache.org/licenses/LICENSE-2.0.txt * * Aaron Zeckoski (azeckoski @ gmail.com) (aaronz @ vt.edu) (aaron @ caret.cam.ac.uk) */ package org.azeckoski.reflectutils; import org.azeckoski.reflectutils.ClassFields.FieldsFilter; import org.azeckoski.reflectutils.ClassProperty.IndexedProperty; import org.azeckoski.reflectutils.ClassProperty.MappedProperty; import org.azeckoski.reflectutils.beanutils.DefaultResolver; import org.azeckoski.reflectutils.beanutils.FieldAdapter; import org.azeckoski.reflectutils.beanutils.FieldAdapterManager; import org.azeckoski.reflectutils.beanutils.Resolver; import org.azeckoski.reflectutils.exceptions.FieldGetValueException; import org.azeckoski.reflectutils.exceptions.FieldSetValueException; import org.azeckoski.reflectutils.exceptions.FieldnameNotFoundException; import org.azeckoski.reflectutils.map.ArrayOrderedMap; import java.lang.ref.SoftReference; import java.lang.reflect.Array; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.List; import java.util.Map; /** * Class which provides methods for dealing with the fields in objects and classes, * this provides the core functionality for the reflect util class<br/> * <br/> * Setting and getting fields supports simple, nested, indexed, and mapped values:<br/> * <b>Simple:</b> Get/set a field in a bean (or map), Example: "title", "id"<br/> * <b>Nested:</b> Get/set a field in a bean which is contained in another bean, Example: "someBean.title", "someBean.id"<br/> * <b>Indexed:</b> Get/set a list/array item by index in a bean, Example: "myList[1]", "anArray[2]"<br/> * <b>Mapped:</b> Get/set a map entry by key in a bean, Example: "myMap(key)", "someMap(thing)"<br/> * * @author Aaron Zeckoski (azeckoski @ gmail.com) */ public class FieldUtils { /** * Empty constructor - protected */ protected FieldUtils() { this( null ); } /** * Constructor which allows the field path name resolver to be set specifically * <br/> * <b>WARNING:</b> if you don't need this control then just use the {@link #getInstance()} method to get this * * @param resolver controls the resolution of indexed, nested, and mapped field paths */ @SuppressWarnings("SameParameterValue") public FieldUtils(Resolver resolver) { setResolver(resolver); fieldAdapterManager = new FieldAdapterManager(); FieldUtils.setInstance(this); } protected ClassDataCacher getClassDataCacher() { return ClassDataCacher.getInstance(); } public ConstructorUtils getConstructorUtils() { return ConstructorUtils.getInstance(); } public ConversionUtils getConversionUtils() { return ConversionUtils.getInstance(); } protected Resolver nameResolver = null; public void setResolver(Resolver resolver) { if (resolver != null) { this.nameResolver = resolver; } else { getResolver(); } } protected Resolver getResolver() { if (nameResolver == null) { nameResolver = new DefaultResolver(); } return nameResolver; } protected FieldAdapterManager fieldAdapterManager; /** * INTERNAL USAGE * @return the field adapter being used by this set of field utils */ public FieldAdapter getFieldAdapter() { return fieldAdapterManager.getFieldAdapter(); } /** * Analyze a class and produce an object which contains information about it and its fields * @param <T> * @param cls any class * @return the ClassFields analysis object which contains the information about this object class * @throws IllegalArgumentException if class is null or primitive */ public <T> ClassFields<T> analyzeClass(Class<T> cls) { ClassFields<T> cf = getClassDataCacher().getClassFields(cls); return cf; } public <T> ClassFields<T> analyzeClass(Class<T> cls, ClassFields.FieldFindMode mode) { ClassFields<T> cf = getClassDataCacher().getClassFields(cls, mode); return cf; } /** * Analyze an object and produce an object which contains information about it and its fields * @param obj any object * @return the ClassFields analysis object which contains the information about this object class * @throws IllegalArgumentException if obj is null */ @SuppressWarnings("unchecked") public <T> ClassFields<T> analyzeObject(Object obj) { if (obj == null) { throw new IllegalArgumentException("obj cannot be null"); } if (Class.class.equals(obj)) { // this is a class so we should pass it over to the other method return analyzeClass((Class)obj); } Class<T> cls = (Class<T>) obj.getClass(); return analyzeClass(cls); } /** * Finds the type for a field based on the given class and the field name * @param type any class type * @param name the name of a field in this class (can be nested, indexed, mapped, etc.) * @return the type of the field (will be {@link Object} if the type is indeterminate) */ public Class<?> getFieldType(Class<?> type, String name) { if (type == null || name == null) { throw new IllegalArgumentException("type and name must not be null"); } // get the nested class or return Object.class as a cop out while (getResolver().hasNested(name)) { String next = getResolver().next(name); Class<?> nestedClass; if (Object.class.equals(type) || Map.class.isAssignableFrom(type) || getResolver().isMapped(next) || getResolver().isIndexed(next) ) { // these can contain objects or it is Object so we bail out return Object.class; } else { // a real class, hooray, analyze it ClassFields<?> cf = analyzeClass(type); nestedClass = cf.getFieldType(name); } type = nestedClass; name = getResolver().remove(name); } String targetName = getResolver().getProperty(name); // simple name of target field // get the type Class<?> fieldType; if ( ConstructorUtils.isClassObjectHolder(type) || Object.class.equals(type) ) { // special handling for the holder types, needed because attempting to analyze a map or other container will cause a failure fieldType = Object.class; } else { // normal object ClassFields<?> cf = analyzeClass(type); try { fieldType = cf.getFieldType(targetName); } catch (FieldnameNotFoundException fnfe) { // could not find this as a standard field so handle as internal lookup ClassData<?> cd = cf.getClassData(); Field field = getFieldIfPossible(cd, name); if (field == null) { throw new FieldnameNotFoundException("Could not find field with name ("+name+") in class (" + type + ") after extended look into non-visible fields", fnfe); } fieldType = field.getType(); } } // special handling for indexed and mapped names if (getResolver().isIndexed(name) || getResolver().isMapped(name)) { if ( ConstructorUtils.isClassArray(fieldType) ) { // use the array component type fieldType = type.getComponentType(); } else { // default for contained type of holders fieldType = Object.class; } } return fieldType; } /** * Finds the type for a field based on the containing object and the field name * @param obj any object * @param name the name of a field in this object (can be nested, indexed, mapped, etc.) * @return the type of the field (will be {@link Object} if the type is indeterminate) * @throws FieldnameNotFoundException if the name is invalid for this obj * @throws IllegalArgumentException if the params are null */ @SuppressWarnings("unchecked") public Class<?> getFieldType(Object obj, String name) { if (obj == null || name == null) { throw new IllegalArgumentException("obj and name must not be null"); } if (Class.class.equals(obj)) { // this is a class so we should pass it over to the other method return getFieldType((Class<?>)obj, name); // EXIT } if ( Object.class.equals(obj.getClass()) ) { return Object.class; // EXIT } // get the nested object or die while (getResolver().hasNested(name)) { String next = getResolver().next(name); Object nestedBean; if (Map.class.isAssignableFrom(obj.getClass())) { nestedBean = getValueOfMap((Map) obj, next); } else if (getResolver().isMapped(next)) { nestedBean = getMappedValue(obj, next); } else if (getResolver().isIndexed(next)) { nestedBean = getIndexedValue(obj, next); } else { nestedBean = getSimpleValue(obj, next); } if (nestedBean == null) { // no auto create so we have to fail here throw new NullPointerException("Nested traversal failure: null field value for name (" + name + ") on object class (" + obj.getClass() + ") for object: " + obj); } obj = nestedBean; name = getResolver().remove(name); } String targetName = getResolver().getProperty(name); // simple name of target field // get the type Class<?> fieldType; if (fieldAdapterManager.isAdaptableObject(obj)) { fieldType = fieldAdapterManager.getFieldAdapter().getFieldType(obj, targetName); } else if ( ConstructorUtils.isClassObjectHolder(obj.getClass()) || Object.class.equals(obj.getClass()) ) { // special handling for the holder types, needed because attempting to analyze a map or other container will cause a failure fieldType = Object.class; } else { // normal object ClassFields<?> cf = analyzeObject(obj); try { ClassProperty cp = cf.getClassProperty(targetName); fieldType = cp.getType(); } catch (FieldnameNotFoundException fnfe) { // could not find this as a standard field so handle as internal lookup ClassData<?> cd = cf.getClassData(); Field field = getFieldIfPossible(cd, name); if (field == null) { throw new FieldnameNotFoundException("Could not find field with name ("+name+") on object (" + obj + ") after extended look into non-visible fields", fnfe); } fieldType = field.getType(); } } // special handling for indexed and mapped names if (getResolver().isIndexed(name) || getResolver().isMapped(name)) { if ( ConstructorUtils.isClassArray(fieldType) ) { // use the array component type fieldType = fieldType.getComponentType(); } else { // default for contained type of holders fieldType = Object.class; } } return fieldType; } /** * Get the types of the fields of a specific class type <br/> * returns the method names as fields (without the "get"/"is" part and camelCased) * @param type any class * @param filter (optional) indicates the fields to return the types for, can be null for defaults * @return a map of field name -> class type */ public Map<String, Class<?>> getFieldTypes(Class<?> type, FieldsFilter filter) { ClassFields<?> cf = analyzeClass(type, findFieldFindMode(filter)); Map<String, Class<?>> types = cf.getFieldTypes(filter); return types; } private ClassFields.FieldFindMode findFieldFindMode(FieldsFilter filter) { ClassFields.FieldFindMode mode = ClassFields.FieldFindMode.HYBRID; // default if (FieldsFilter.ALL.equals(filter)) { mode = ClassFields.FieldFindMode.ALL; } else if (FieldsFilter.SERIALIZABLE_FIELDS.equals(filter)) { mode = ClassFields.FieldFindMode.FIELD; } return mode; } /** * Get the names of all fields in a class * @param cls any class * @return a list of the field names */ public <T> List<String> getFieldNames(Class<T> cls) { ClassFields<T> cf = analyzeClass(cls); return cf.getFieldNames(); } public <T> List<String> getFieldNames(Class<T> cls, FieldsFilter filter) { ClassFields<T> cf = analyzeClass(cls, findFieldFindMode(filter)); return cf.getFieldNames(); } /** * Get the values of all readable fields on an object (may not all be writeable) * @param obj any object * @return a map of field name -> value * @throws IllegalArgumentException if the obj is null */ public Map<String, Object> getFieldValues(Object obj) { return getFieldValues(obj, FieldsFilter.READABLE, false); } /** * Get the values of all fields on an object but optionally filter the fields used * @param obj any object * @param filter (optional) indicates the fields to return the values for, can be null for defaults <br/> * WARNING: getting the field values from settable only fields works as expected (i.e. you will an empty map) * @param includeClassField if true then the value from the "getClass()" method is returned as part of the * set of object values with a type of {@link Class} and a field name of "class" * @return a map of field name -> value * @throws IllegalArgumentException if the obj is null */ public Map<String, Object> getFieldValues(Object obj, FieldsFilter filter, boolean includeClassField) { if (obj == null) { throw new IllegalArgumentException("obj cannot be null"); } Map<String, Object> values = new ArrayOrderedMap<String, Object>(); if (includeClassField) { // add as the first field values.put(ClassFields.FIELD_CLASS, obj.getClass()); } if (fieldAdapterManager.isAdaptableObject(obj)) { values.putAll( fieldAdapterManager.getFieldAdapter().getFieldValues(obj, filter) ); } else { Map<String, Class<?>> types = getFieldTypes(obj.getClass(), filter); if (FieldsFilter.WRITEABLE.equals(filter)) { types.clear(); } for (String name : types.keySet()) { try { Object o = getFieldValue(obj, name); values.put(name, o); } catch (RuntimeException e) { // failed to get the value so we will skip this one continue; } } } return values; } /** * Get the value of a field on an object, * name can be nested, indexed, or mapped * @param obj any object * @param name the name of a field on this object * @return the value of the field * @throws FieldnameNotFoundException if this field name is invalid for this object * @throws FieldGetValueException if the field is not readable or not visible * @throws IllegalArgumentException if there is a failure getting the value */ @SuppressWarnings("unchecked") public Object getFieldValue(Object obj, String name) { if (obj == null) { throw new IllegalArgumentException("obj cannot be null"); } if (name == null || "".equals(name)) { throw new IllegalArgumentException("field name cannot be null or blank"); } // Resolve nested references Holder holder = unpackNestedName(name, obj, false); name = holder.getName(); obj = holder.getObject(); Object value; if (Map.class.isAssignableFrom(obj.getClass())) { value = getValueOfMap((Map) obj, name); } else if (getResolver().isMapped(name)) { value = getMappedValue(obj, name); } else if (getResolver().isIndexed(name)) { value = getIndexedValue(obj, name); } else { value = getSimpleValue(obj, name); } return value; } /** * Get the value of a field on an object as a specific type, * name can be nested, indexed, or mapped * @param obj any object * @param name the name of a field on this object * @param asType the type to return the value as (converts as needed) * @return the value in the field as the type requested * @throws FieldnameNotFoundException if this field name is invalid for this object * @throws FieldGetValueException if the field is not readable or not visible * @throws UnsupportedOperationException if the value cannot be converted to the type requested * @throws IllegalArgumentException if there is a failure getting the value */ public <T> T getFieldValue(Object obj, String name, Class<T> asType) { Object o = getFieldValue(obj, name); T value = getConversionUtils().convert(o, asType); return value; } /** * Set the value of a field on an object (automatically auto converts), * name can be nested, indexed, or mapped * * @param obj any object * @param name the name of a field on this object * @param value the value to set the field to (must match target exactly) * @throws FieldnameNotFoundException if this field name is invalid for this object * @throws FieldSetValueException if the field is not writeable or visible * @throws IllegalArgumentException if there is a general failure setting the value */ public void setFieldValue(Object obj, String name, Object value) { setFieldValue(obj, name, value, true); } /** * Set the value of a field on an object (optionally convert the value to the field type), * name can be nested, indexed, or mapped * * @param obj any object * @param name the name of a field on this object * @param value the value to set the field to * @param autoConvert if true then the value will be converted to the target value type if it is possible, * otherwise the value must match the target type exactly * @throws FieldnameNotFoundException if this field name is invalid for this object * @throws UnsupportedOperationException if this value cannot be auto converted to the type specified * @throws FieldSetValueException if the field is not writeable or visible * @throws IllegalArgumentException if there is a general failure setting the value */ @SuppressWarnings("unchecked") public void setFieldValue(Object obj, String name, Object value, boolean autoConvert) { if (obj == null) { throw new IllegalArgumentException("obj cannot be null"); } if (name == null || "".equals(name)) { throw new IllegalArgumentException("field name cannot be null or blank"); } // Resolve nested references Holder holder = unpackNestedName(name, obj, true); name = holder.getName(); obj = holder.getObject(); if (autoConvert) { // auto convert the value to the target type if possible // String targetName = getResolver().getProperty(name); // simple name of target field // attempt to convert the value into the target type Class<?> type = getFieldType(obj, name); value = getConversionUtils().convert(value, type); } // set the value if (Map.class.isAssignableFrom(obj.getClass())) { setValueOfMap((Map) obj, name, value); } else if (getResolver().isMapped(name)) { setMappedValue(obj, name, value); } else if (getResolver().isIndexed(name)) { setIndexedValue(obj, name, value); } else { setSimpleValue(obj, name, value); } } /** * For setting an indexed value on an indexed object directly, * indexed objects are lists and arrays <br/> * NOTE: If the supplied index is invalid for the array then this will fail * * @param indexedObject any array or list * @param index the index to put the value into (will append to the end of the list if index < 0), must be within the bounds of the array * @param value any value, will be converted to the correct type for the array automatically * @throws IllegalArgumentException if there is a failure because of an invalid index or null arguments * @throws FieldSetValueException if the field is not writeable or visible */ @SuppressWarnings("unchecked") public void setIndexedValue(Object indexedObject, int index, Object value) { if (indexedObject == null) { throw new IllegalArgumentException("Invalid indexedObject, cannot be null"); } if ( ConstructorUtils.isClassArray(indexedObject.getClass()) ) { // this is an array try { // set the value on the array // NOTE: cannot automatically expand the array Class<?> componentType = ArrayUtils.type((Object[])indexedObject); Object convert = ReflectUtils.getInstance().convert(value, componentType); Array.set(indexedObject, index, convert); } catch (Exception e) { throw new IllegalArgumentException("Failed to set index ("+index+") for array of size ("+Array.getLength(indexedObject)+") to value: " + value, e); } } else if ( ConstructorUtils.isClassList(indexedObject.getClass()) ) { // this is a list List l = (List) indexedObject; try { // set value on list if (index < 0) { l.add(value); } else { if (index >= l.size()) { // automatically expand the list int start = l.size(); for (int i = start; i < (index+1); i++) { l.add(i, null); } } l.set(index, value); } } catch (Exception e) { // catching the general exception is correct here, translate the exception throw new IllegalArgumentException("Failed to set index ("+index+") for list of size ("+l.size()+") to value: " + value, e); } } else { // invalid //noinspection ConstantConditions throw new IllegalArgumentException("Object does not appear to be indexed (not an array or a list): " + (indexedObject == null ? "NULL" : indexedObject.getClass()) ); } } // INTERNAL - specific methods which are not really for general use /** * Traverses the nested name path to get to the requested name and object * @param fullName the full path name (e.g. thing.field1.field2.stuff) * @param object the object to traverse * @param autoCreate if true then create the nested objects to force successful traversal, else will throw NPE * @return a holder with the nested name (e.g. stuff) and the nested object * @throws NullPointerException if the path cannot be traversed * @throws IllegalArgumentException if the path is invalid */ @SuppressWarnings("unchecked") protected Holder unpackNestedName(final String fullName, final Object object, boolean autoCreate) { String name = fullName; Object obj = object; Class<?> cls = object.getClass(); try { // Resolve nested references while (getResolver().hasNested(name)) { String next = getResolver().next(name); Object nestedBean; if (Map.class.isAssignableFrom(obj.getClass())) { nestedBean = getValueOfMap((Map) obj, next); } else if (getResolver().isMapped(next)) { nestedBean = getMappedValue(obj, next); } else if (getResolver().isIndexed(next)) { nestedBean = getIndexedValue(obj, next); } else { nestedBean = getSimpleValue(obj, next); } if (nestedBean == null) { // could not get the nested bean because it is unset if (autoCreate) { // create the nested bean try { Class<?> type = getFieldType(obj, next); if (Object.class.equals(type)) { // indeterminate type so we will make a map type = ArrayOrderedMap.class; } nestedBean = getConstructorUtils().constructClass(type); setFieldValue(obj, next, nestedBean, false); // need to put this new object into the parent } catch (RuntimeException e) { throw new IllegalArgumentException("Nested path failure: Could not create nested object (" +cls.getName()+") in path ("+fullName+"): " + e.getMessage(), e); } } else { // no auto create so we have to fail here throw new NullPointerException("Nested traversal failure: null field value for name (" + name + ") in nestedName ("+fullName+") on object class (" + cls + ") for object: " + obj); } } obj = nestedBean; name = getResolver().remove(name); } } catch (FieldnameNotFoundException e) { // convert field name failure into illegal argument throw new IllegalArgumentException("Nested path failure: Invalid path name (" +fullName+") contains invalid field names: " + e.getMessage(), e); } return new Holder(name, obj); } /** * For getting a value out of a bean based on a field name * <br/> * <b>WARNING: Cannot handle a nested/mapped/indexed name</b> * @throws FieldnameNotFoundException if this field name is invalid for this object * @throws IllegalArgumentException if there is failure */ protected Object getSimpleValue(Object obj, String name) { if (obj == null) { throw new IllegalArgumentException("obj cannot be null"); } if (name == null || "".equals(name)) { throw new IllegalArgumentException("field name cannot be null or blank"); } Object value; // Handle DynaBean instances specially if (fieldAdapterManager.isAdaptableObject(obj)) { value = fieldAdapterManager.getFieldAdapter().getSimpleValue(obj, name); } else { // normal bean ClassFields<?> cf = analyzeObject(obj); try { // use the class property ClassProperty cp = cf.getClassProperty(name); value = findFieldValue(obj, cp); } catch (FieldnameNotFoundException fnfe) { // could not find this as a standard field so handle as internal lookup ClassData<?> cd = cf.getClassData(); Field field = getFieldIfPossible(cd, name); if (field == null) { throw new FieldnameNotFoundException("Could not find field with name ("+name+") on object (" + obj + ") after extended look into non-visible fields", fnfe); } try { value = field.get(obj); } catch (Exception e) { // catching the general exception is correct here, translate the exception throw new FieldGetValueException("Field get failure getting value for field ("+name+") from non-visible field in object: " + obj, name, obj, e); } } } return value; } /** * For getting an indexed value out of an object based on field name, * name must be like: fieldname[index], * If the object is an array or a list then name can be the index only: * "[1]" or "[0]" (brackets must be included) * <br/> * <b>WARNING: Cannot handle a nested/mapped/indexed name</b> * @throws FieldnameNotFoundException if this field name is invalid for this object * @throws IllegalArgumentException if there is a failure * @throws FieldGetValueException if there is an internal failure getting the field */ @SuppressWarnings("unchecked") protected Object getIndexedValue(Object obj, String name) { if (obj == null) { throw new IllegalArgumentException("obj cannot be null"); } if (name == null || "".equals(name)) { throw new IllegalArgumentException("field name cannot be null or blank"); } Object value = null; Resolver resolver = getResolver(); // get the index from the indexed name int index; try { index = resolver.getIndex(name); if (index < 0) { throw new IllegalArgumentException("Could not find index in name (" + name + ")"); } // get the fieldname from the indexed name name = resolver.getProperty(name); } catch (IllegalArgumentException e) { throw new IllegalArgumentException("Invalid indexed field (" + name + ") on type (" + obj.getClass() + ")", e); } boolean indexedProperty = false; // Handle DynaBean instances specially if (fieldAdapterManager.isAdaptableObject(obj)) { value = fieldAdapterManager.getFieldAdapter().getIndexedValue(obj, name, index); } else { boolean isArray = false; Object indexedObject = null; if (obj.getClass().isArray()) { indexedObject = obj; isArray = true; } else if (List.class.isAssignableFrom(obj.getClass())) { indexedObject = obj; } else { // normal bean ClassFields cf = analyzeObject(obj); ClassProperty cp = cf.getClassProperty(name); if (! cp.isIndexed()) { throw new IllegalArgumentException("This field ("+name+") is not an indexed field"); } isArray = cp.isArray(); // try to get the indexed getter and use that first if (cp instanceof IndexedProperty) { indexedProperty = true; IndexedProperty icp = (IndexedProperty) cp; try { Method getter = icp.getIndexGetter(); //noinspection RedundantArrayCreation value = getter.invoke(obj, new Object[] {index}); } catch (Exception e) { // catching the general exception is correct here, translate the exception throw new FieldGetValueException("Indexed getter method failure getting indexed (" +index+") value for name ("+cp.getFieldName()+") from: " + obj, cp.getFieldName(), obj, e); } } else { indexedObject = findFieldValue(obj, cp); } } if (!indexedProperty) { // now get the indexed value if possible if (indexedObject != null) { if (isArray) { // this is an array try { // get value from array value = Array.get(indexedObject, index); } catch (ArrayIndexOutOfBoundsException e) { throw new IllegalArgumentException("Index ("+index+") is out of bounds ("+Array.getLength(indexedObject)+") for the array: " + indexedObject, e); } } else { // this better be a list if (! List.class.isAssignableFrom(indexedObject.getClass())) { throw new IllegalArgumentException("Field (" + name + ") does not appear to be indexed (not an array or a list)"); } else { // get value from list try { value = ((List)indexedObject).get(index); } catch (IndexOutOfBoundsException e) { throw new IllegalArgumentException("Index ("+index+") is out of bounds ("+((List)indexedObject).size()+") for the list: " + indexedObject, e); } } } } else { throw new IllegalArgumentException("Indexed object is null, cannot retrieve index ("+index+") value from field ("+name+")"); } } } return value; } /** * For getting a mapped value out of an object based on field name, * name must be like: fieldname[index] * <br/> * <b>WARNING: Cannot handle a nested/mapped/indexed name</b> * @throws FieldnameNotFoundException if this field name is invalid for this object * @throws IllegalArgumentException if there are invalid arguments * @throws FieldGetValueException if there is an internal failure getting the field */ @SuppressWarnings("unchecked") protected Object getMappedValue(Object obj, String name) { if (obj == null) { throw new IllegalArgumentException("obj cannot be null"); } if (name == null || "".equals(name)) { throw new IllegalArgumentException("field name cannot be null or blank"); } Object value = null; Resolver resolver = getResolver(); // get the key from the mapped name String key; try { key = resolver.getKey(name); if (key == null) { throw new IllegalArgumentException("Could not find key in name (" + name + ")"); } // get the fieldname from the mapped name name = resolver.getProperty(name); } catch (IllegalArgumentException e) { throw new IllegalArgumentException("Invalid mapped field (" + name + ") on type (" + obj.getClass() + ")", e); } boolean mappedProperty = false; // Handle DynaBean instances specially if (fieldAdapterManager.isAdaptableObject(obj)) { value = fieldAdapterManager.getFieldAdapter().getMappedValue(obj, name, key); } else { Map map = null; if (Map.class.isAssignableFrom(obj.getClass())) { map = (Map) obj; } else { // normal bean ClassFields cf = analyzeObject(obj); ClassProperty cp = cf.getClassProperty(name); if (! cp.isMapped()) { throw new IllegalArgumentException("This field ("+name+") is not an mapped field"); } // try to get the mapped getter and use that first if (cp instanceof MappedProperty) { mappedProperty = true; MappedProperty mcp = (MappedProperty) cp; try { Method getter = mcp.getMapGetter(); //noinspection RedundantArrayCreation value = getter.invoke(obj, new Object[] {key}); } catch (Exception e) { // catching the general exception is correct here, translate the exception throw new FieldGetValueException("Mapped getter method failure getting mapped (" +key+") value for name ("+cp.getFieldName()+") from: " + obj, cp.getFieldName(), obj, e); } } else { Object o = findFieldValue(obj, cp); if (! Map.class.isAssignableFrom(o.getClass())) { throw new IllegalArgumentException("Field (" + name + ") does not appear to be a map (not instance of Map)"); } map = (Map) o; } } // get the value from the map if (!mappedProperty) { if (map != null) { try { value = map.get(key); } catch (Exception e) { throw new IllegalArgumentException("Key ("+key+") is invalid ("+map.size()+") for the map: " + map, e); } } else { throw new IllegalArgumentException("Mapped object is null, cannot retrieve key ("+key+") value from field ("+name+")"); } } } return value; } /** * For getting a value out of a map based on a field name which has a key in it, * name is expected to be the key for the map only: e.g. "mykey", * if it happens to be of the form: thing[mykey] then the key will be extracted * <br/> * <b>WARNING: Cannot handle a nested name or mapped/indexed key</b> * @return the value in the map with a key matching this name OR null if no key found */ @SuppressWarnings("unchecked") protected Object getValueOfMap(Map map, String name) { Resolver resolver = getResolver(); if (resolver.isMapped(name)) { String propName = resolver.getProperty(name); if (propName == null || propName.length() == 0) { name = resolver.getKey(name); } } Object value = map.get(name); return value; } /** * This will get the value from a field, * for internal use only, * Reduce code duplication * @param obj any object * @param cp the analysis object which must match the given object (defines the field) * @return the value for the field * @throws IllegalArgumentException if inputs are invalid (null) * @throws FieldGetValueException if there is an internal failure getting the field */ protected Object findFieldValue(Object obj, ClassProperty cp) { if (obj == null) { throw new IllegalArgumentException("Object cannot be null"); } if (cp == null) { throw new IllegalArgumentException("ClassProperty cannot be null"); } Object value; if (cp.isPublicField()) { Field field = cp.getField(); try { value = field.get(obj); } catch (Exception e) { // catching the general exception is correct here, translate the exception throw new FieldGetValueException("Field get failure getting value for name ("+cp.getFieldName()+") from: " + obj, cp.getFieldName(), obj, e); } } else { // must be a property then Method getter = cp.getGetter(); try { //noinspection RedundantArrayCreation value = getter.invoke(obj, new Object[0]); } catch (Exception e) { // catching the general exception is correct here, translate the exception throw new FieldGetValueException("Getter method failure getting value for name ("+cp.getFieldName()+") from: " + obj, cp.getFieldName(), obj, e); } } return value; } /** * Set a value on a field of an object, the types must match and the name must be identical * <br/> * <b>WARNING: Cannot handle a nested/mapped/indexed name</b> * @throws FieldnameNotFoundException if this field name is invalid for this object * @throws IllegalArgumentException if there is failure * @throws FieldSetValueException if there is an internal failure setting the field */ protected void setSimpleValue(Object obj, String name, Object value) { if (obj == null) { throw new IllegalArgumentException("obj cannot be null"); } if (name == null || "".equals(name)) { throw new IllegalArgumentException("field name cannot be null or blank"); } // Handle DynaBean instances specially if (fieldAdapterManager.isAdaptableObject(obj)) { fieldAdapterManager.getFieldAdapter().setSimpleValue(obj, name, value); } else { // normal bean ClassFields<?> cf = analyzeObject(obj); try { ClassProperty cp = cf.getClassProperty(name); assignFieldValue(obj, cp, value); } catch (FieldnameNotFoundException fnfe) { // could not find this as a standard field so handle as internal lookup ClassData<?> cd = cf.getClassData(); Field field = getFieldIfPossible(cd, name); if (field == null) { throw new FieldnameNotFoundException("Could not find field with name ("+name+") on object (" + obj + ") after extended look into non-visible fields", fnfe); } try { value = getConversionUtils().convert(value, field.getType()); field.set(obj, value); } catch (Exception e) { // catching the general exception is correct here, translate the exception throw new FieldSetValueException("Field set failure setting value ("+value+") for field ("+name+") from non-visible field in object: " + obj, name, obj, e); } } } } /** * For setting an indexed value on an object based on field name, * name must be like: fieldname[index] * <br/> * <b>WARNING: Cannot handle a nested/mapped/indexed name</b> * @throws FieldnameNotFoundException if this field name is invalid for this object * @throws IllegalArgumentException if there is a failure * @throws FieldSetValueException if there is an internal failure setting the field */ @SuppressWarnings("unchecked") protected void setIndexedValue(Object obj, String name, Object value) { if (obj == null) { throw new IllegalArgumentException("obj cannot be null"); } if (name == null || "".equals(name)) { throw new IllegalArgumentException("field name cannot be null or blank"); } Resolver resolver = getResolver(); // get the index from the indexed name int index; try { index = resolver.getIndex(name); if (index < 0) { throw new IllegalArgumentException("Could not find index in name (" + name + ")"); } // get the fieldname from the indexed name name = resolver.getProperty(name); } catch (IllegalArgumentException e) { throw new IllegalArgumentException("Invalid indexed field (" + name + ") on type (" + obj.getClass() + ")", e); } boolean indexedProperty = false; // Handle DynaBean instances specially if (fieldAdapterManager.isAdaptableObject(obj)) { fieldAdapterManager.getFieldAdapter().setIndexedValue(obj, name, index, value); } else { boolean isArray = false; Object indexedObject = null; if ( ConstructorUtils.isClassArray(obj.getClass()) ) { indexedObject = obj; isArray = true; } else if ( ConstructorUtils.isClassList(obj.getClass()) ) { indexedObject = obj; } else { // normal bean ClassFields cf = analyzeObject(obj); ClassProperty cp = cf.getClassProperty(name); if (! cp.isIndexed()) { throw new IllegalArgumentException("This field ("+name+") is not an indexed field"); } isArray = cp.isArray(); // try to get the indexed setter and use that first if (cp instanceof IndexedProperty) { indexedProperty = true; IndexedProperty icp = (IndexedProperty) cp; try { Method setter = icp.getIndexSetter(); //noinspection RedundantArrayCreation setter.invoke(obj, new Object[] {index, value}); } catch (Exception e) { // catching the general exception is correct here, translate the exception throw new FieldSetValueException("Indexed setter method failure setting indexed (" +index+") value for name ("+cp.getFieldName()+") on: " + obj, cp.getFieldName(), obj, e); } } else { // get the field value out and work with it directly indexedObject = findFieldValue(obj, cp); if (indexedObject == null) { // handle nulls by creating if possible try { if (isArray) { // create the array if it is null Class<?> type = value.getClass(); indexedObject = ArrayUtils.create(type, index+1); } else { // List // create the list if it is null, back-fill it, and assign it back to the object Class<?> type = cp.getType(); if (type.isInterface()) { indexedObject = new ArrayList(index+1); } else { indexedObject = type.newInstance(); } } setSimpleValue(obj, name, indexedObject); } catch (Exception e) { throw new IllegalArgumentException("Indexed object is null, attempt to create list failed, cannot set value for index ("+index+") on field ("+name+")", e); } } } } if (!indexedProperty) { // set the indexed value if (isArray) { // this is an array try { // set the value on the array int length = ArrayUtils.size((Object[])indexedObject); if (index >= length) { // automatically expand the array indexedObject = ArrayUtils.resize((Object[])indexedObject, index+1); setSimpleValue(obj, name, indexedObject); // need to put the array back into the object } // convert this value to the type for the array Class<?> componentType = ArrayUtils.type((Object[])indexedObject); Object convert = ReflectUtils.getInstance().convert(value, componentType); Array.set(indexedObject, index, convert); } catch (Exception e) { throw new IllegalArgumentException("Failed to set index ("+index+") for array of size ("+Array.getLength(indexedObject)+") to value: " + value, e); } } else { // this better be a list if (indexedObject == null || ! List.class.isAssignableFrom(indexedObject.getClass())) { throw new IllegalArgumentException("Field (" + name + ") does not appear to be indexed (not an array or a list): " + (indexedObject == null ? "NULL" : indexedObject.getClass()) ); } else { // this is a list List l = (List) indexedObject; try { // set value on list if (index < 0) { l.add(value); } else { if (index >= l.size()) { // automatically expand the list int start = l.size(); for (int i = start; i < (index+1); i++) { l.add(i, null); } } l.set(index, value); } } catch (Exception e) { // catching the general exception is correct here, translate the exception throw new IllegalArgumentException("Failed to set index ("+index+") for list of size ("+l.size()+") to value: " + value, e); } } } } } } /** * For getting a mapped value out of an object based on field name, * name must be like: fieldname[index] * <br/> * <b>WARNING: Cannot handle a nested/mapped/indexed name</b> * @throws FieldnameNotFoundException if this field name is invalid for this object * @throws IllegalArgumentException if there is failure * @throws FieldSetValueException if there is an internal failure setting the field */ @SuppressWarnings("unchecked") protected void setMappedValue(Object obj, String name, Object value) { if (obj == null) { throw new IllegalArgumentException("obj cannot be null"); } if (name == null || "".equals(name)) { throw new IllegalArgumentException("field name cannot be null or blank"); } Resolver resolver = getResolver(); // get the key from the mapped name String key; try { key = resolver.getKey(name); if (key == null) { throw new IllegalArgumentException("Could not find key in name (" + name + ")"); } // get the fieldname from the mapped name name = resolver.getProperty(name); } catch (IllegalArgumentException e) { throw new IllegalArgumentException("Invalid mapped field (" + name + ") on type (" + obj.getClass() + ")", e); } boolean mappedProperty = false; // Handle DynaBean instances specially if (fieldAdapterManager.isAdaptableObject(obj)) { fieldAdapterManager.getFieldAdapter().setMappedValue(obj, name, key, value); } else { Map map = null; if (Map.class.isAssignableFrom(obj.getClass())) { map = (Map) obj; } else { // normal bean ClassFields cf = analyzeObject(obj); ClassProperty cp = cf.getClassProperty(name); if (! cp.isMapped()) { throw new IllegalArgumentException("This field ("+name+") is not an mapped field"); } // try to get the mapped setter and use that first if (cp instanceof MappedProperty) { mappedProperty = true; MappedProperty mcp = (MappedProperty) cp; try { Method setter = mcp.getMapSetter(); //noinspection RedundantArrayCreation value = setter.invoke(obj, new Object[] {key, value}); } catch (Exception e) { // catching the general exception is correct here, translate the exception throw new FieldSetValueException("Mapped setter method failure setting mapped (" +key+") value for name ("+cp.getFieldName()+") on: " + obj, cp.getFieldName(), obj, e); } } else { Object o = findFieldValue(obj, cp); if (o == null) { // create the map if it is null and assign it back to the object try { Class<?> type = cp.getType(); if (type.isInterface()) { map = new ArrayOrderedMap(5); } else { map = (Map) type.newInstance(); } setSimpleValue(obj, name, map); } catch (Exception e) { // catching the general exception is correct here, translate the exception throw new IllegalArgumentException("Mapped object is null, attempt to create map failed, cannot set value for key ("+key+") on field ("+name+")", e); } } else { if (! Map.class.isAssignableFrom(o.getClass())) { throw new IllegalArgumentException("Field (" + name + ") does not appear to be a map (not instance of Map)"); } map = (Map) o; } } } if (!mappedProperty) { // set value in map if (map == null) { throw new IllegalArgumentException("Mapped object is null, cannot set value for key ("+key+") on field ("+name+")"); } // set value on map try { map.put(key, value); } catch (Exception e) { throw new IllegalArgumentException("Value ("+value+") cannot be put for key ("+key+") for the map: " + map, e); } } } } /** * Set a value on a map using the name as the key, * name is expected to be the key for the map only: e.g. "mykey", * if it happens to be of the form: thing[mykey] then the key will be extracted * <br/> * <b>WARNING: Cannot handle a nested name or mapped/indexed key</b> */ @SuppressWarnings("unchecked") protected void setValueOfMap(Map map, String name, Object value) { Resolver resolver = getResolver(); if (resolver.isMapped(name)) { String propName = resolver.getProperty(name); if (propName == null || propName.length() == 0) { name = resolver.getKey(name); } } map.put(name, value); } /** * This will set the value on a field, types must match, * for internal use only, * Reduce code duplication * @param obj any object * @param cp the analysis object which must match the given name and object * @param value the value for the field * @throws FieldSetValueException if the field is not writeable or visible * @throws IllegalArgumentException if inputs are invalid (null) */ protected void assignFieldValue(Object obj, ClassProperty cp, Object value) { if (obj == null) { throw new IllegalArgumentException("Object cannot be null"); } if (cp == null) { throw new IllegalArgumentException("ClassProperty cannot be null"); } if (cp.isPublicField()) { Field field = cp.getField(); try { field.set(obj, value); } catch (Exception e) { // catching the general exception is correct here, translate the exception throw new FieldSetValueException("Field set failure setting value ("+value+") for name ("+cp.getFieldName()+") on: " + obj, cp.getFieldName(), value, obj, e); } } else { // must be a property then Method setter = cp.getSetter(); try { //noinspection RedundantArrayCreation setter.invoke(obj, new Object[] {value}); } catch (Exception e) { throw new FieldSetValueException("Setter method failure setting value ("+value+") for name ("+cp.getFieldName()+") on: " + obj, cp.getFieldName(), value, obj, e); } } } /** * Get the field if it exists for this class * @param cd the class data cache object * @param name the name of the field * @return the field if found OR null if not */ protected Field getFieldIfPossible(ClassData<?> cd, String name) { Field f = null; List<Field> fields = cd.getFields(); for (Field field : fields) { if (field.getName().equals(name)) { f = field; break; } } return f; } public static final class Holder { public String name; public Object object; public Holder(String name, Object object) { this.name = name; this.object = object; } public String getName() { return name; } public Object getObject() { return object; } } @Override public String toString() { return "Field::c="+FieldUtils.timesCreated+":s="+singleton+":resolver=" + getResolver().getClass().getName(); } // STATIC access protected static SoftReference<FieldUtils> instanceStorage; /** * Get a singleton instance of this class to work with (stored statically) <br/> * <b>WARNING</b>: do not hold onto this object or cache it yourself, call this method again if you need it again * @return a singleton instance of this class */ public static FieldUtils getInstance() { FieldUtils instance = (instanceStorage == null ? null : instanceStorage.get()); if (instance == null) { instance = FieldUtils.setInstance(null); } return instance; } /** * Set the singleton instance of the class which will be stored statically * @param newInstance the instance to use as the singleton instance */ public static FieldUtils setInstance(FieldUtils newInstance) { FieldUtils instance = newInstance; if (instance == null) { instance = new FieldUtils(); instance.singleton = true; } FieldUtils.timesCreated++; instanceStorage = new SoftReference<FieldUtils>(instance); return instance; } public static void clearInstance() { instanceStorage.clear(); } private static int timesCreated = 0; public static int getTimesCreated() { return timesCreated; } private boolean singleton = false; public boolean isSingleton() { return singleton; } }