com.twelvemonkeys.lang.BeanUtil Maven / Gradle / Ivy
/*
* Copyright (c) 2008, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.lang;
import com.twelvemonkeys.util.convert.ConversionException;
import com.twelvemonkeys.util.convert.Converter;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.Map;
/**
* A utility class with some useful bean-related functions.
*
* NOTE: This class is not considered part of the public API and may be changed without notice
*
*
* @author Harald Kuhr
* @author last modified by $Author: haku $
*
* @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/lang/BeanUtil.java#2 $
*/
public final class BeanUtil {
// Disallow creating objects of this type
private BeanUtil() {
}
/**
* Gets a property value from the given object, using reflection.
* Now supports getting values from properties of properties
* (recursive).
*
* @param pObject The object to get the property from
* @param pProperty The name of the property
*
* @return A string containing the value of the given property, or {@code null}
* if it can not be found.
*/
public static Object getPropertyValue(Object pObject, String pProperty) {
// TODO: Remove System.err's... Create new Exception? Hmm..
// TODO: Support get(Object) method of Collections!
// Handle lists and arrays with [] (index) operator
if (pObject == null || pProperty == null || pProperty.length() < 1) {
return null;
}
Class> objClass = pObject.getClass();
Object result = pObject;
// Method for method...
String subProp;
int begIdx = 0;
int endIdx = begIdx;
while (begIdx < pProperty.length() && begIdx >= 0) {
endIdx = pProperty.indexOf('.', endIdx + 1);
if (endIdx > 0) {
subProp = pProperty.substring(begIdx, endIdx);
begIdx = endIdx + 1;
}
else {
// The final property!
// If there's just the first-level property, subProp will be
// equal to property
subProp = pProperty.substring(begIdx);
begIdx = -1;
}
// Check for "[" and "]"
Object[] param = null;
Class[] paramClass = new Class[0];
int begBracket;
if ((begBracket = subProp.indexOf('[')) > 0) {
// An error if there is no matching bracket
if (!subProp.endsWith("]")) {
return null;
}
String between = subProp.substring(begBracket + 1,
subProp.length() - 1);
subProp = subProp.substring(0, begBracket);
// If brackets exist, check type of argument between brackets
param = new Object[1];
paramClass = new Class[1];
//try {
// TODO: isNumber returns true, even if too big for integer...
if (StringUtil.isNumber(between)) {
// We have a number
// Integer -> array subscript -> getXXX(int i)
try {
// Insert param and it's Class
param[0] = Integer.valueOf(between);
paramClass[0] = Integer.TYPE; // int.class
}
catch (NumberFormatException e) {
// ??
// Probably too small or too large value..
}
}
else {
//catch (NumberFormatException e) {
// Not a number... Try String
// String -> Hashtable key -> getXXX(String str)
// Insert param and it's Class
param[0] = between.toLowerCase();
paramClass[0] = String.class;
}
}
Method method;
String methodName = "get" + StringUtil.capitalize(subProp);
try {
// Try to get the "get" method for the given property
method = objClass.getMethod(methodName, paramClass);
}
catch (NoSuchMethodException e) {
System.err.print("No method named \"" + methodName + "()\"");
// The array might be of size 0...
if (paramClass.length > 0 && paramClass[0] != null) {
System.err.print(" with the parameter " + paramClass[0].getName());
}
System.err.println(" in class " + objClass.getName() + "!");
return null;
}
// If method for some reason should be null, give up
if (method == null) {
return null;
}
try {
// We have a method, try to invoke it
// The resutling object will be either the property we are
// Looking for, or the parent
// System.err.println("Trying " + objClass.getName() + "." + method.getName() + "(" + ((param != null && param.length > 0) ? param[0] : "") + ")");
result = method.invoke(result, param);
}
catch (InvocationTargetException e) {
System.err.println("property=" + pProperty + " & result=" + result + " & param=" + Arrays.toString(param));
e.getTargetException().printStackTrace();
e.printStackTrace();
return null;
}
catch (IllegalAccessException e) {
e.printStackTrace();
return null;
}
catch (NullPointerException e) {
System.err.println(objClass.getName() + "." + method.getName() + "(" + ((paramClass.length > 0 && paramClass[0] != null) ? paramClass[0].getName() : "") + ")");
e.printStackTrace();
return null;
}
if (result != null) {
// Get the class of the reulting object
objClass = result.getClass();
}
else {
return null;
}
} // while
return result;
}
/**
* Sets the property value to an object using reflection.
* Supports setting values of properties that are properties of
* properties (recursive).
*
* @param pObject The object to get a property from
* @param pProperty The name of the property
* @param pValue The property value
*
* @throws NoSuchMethodException if there's no write method for the
* given property
* @throws InvocationTargetException if invoking the write method failed
* @throws IllegalAccessException if the caller class has no access to the
* write method
*/
public static void setPropertyValue(Object pObject, String pProperty, Object pValue)
throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
//
// TODO: Support set(Object, Object)/put(Object, Object) methods
// of Collections!
// Handle lists and arrays with [] (index) operator
Class paramType = pValue != null ? pValue.getClass() : Object.class;
// Preserve references
Object obj = pObject;
String property = pProperty;
// Recurse and find real parent if property contains a '.'
int dotIdx = property.indexOf('.');
if (dotIdx >= 0) {
// Get real parent
obj = getPropertyValue(obj, property.substring(0, dotIdx));
// Get the property of the parent
property = property.substring(dotIdx + 1);
}
// Find method
Object[] params = {pValue};
Method method = getMethodMayModifyParams(obj, "set" + StringUtil.capitalize(property),
new Class[] {paramType}, params);
// Invoke it
method.invoke(obj, params);
}
private static Method getMethodMayModifyParams(Object pObject, String pName, Class[] pParams, Object[] pValues)
throws NoSuchMethodException {
// NOTE: This method assumes pParams.length == 1 && pValues.length == 1
Method method = null;
Class paramType = pParams[0];
try {
method = pObject.getClass().getMethod(pName, pParams);
}
catch (NoSuchMethodException e) {
// No direct match
// 1: If primitive wrapper, try unwrap conversion first
/*if (paramType.isPrimitive()) { // NOTE: Can't be primitive type
params[0] = ReflectUtil.wrapType(paramType);
}
else*/ if (ReflectUtil.isPrimitiveWrapper(paramType)) {
pParams[0] = ReflectUtil.unwrapType(paramType);
}
try {
// If this does not throw an exception, it works
method = pObject.getClass().getMethod(pName, pParams);
}
catch (Throwable t) {
// Ignore
}
// 2: Try any super-types of paramType, to see if we have a match
if (method == null) {
while ((paramType = paramType.getSuperclass()) != null) {
pParams[0] = paramType;
try {
// If this does not throw an exception, it works
method = pObject.getClass().getMethod(pName, pParams);
}
catch (Throwable t) {
// Ignore/Continue
continue;
}
break;
}
}
// 3: Try to find a different method with the same name, that has
// a parameter type we can convert to...
// NOTE: There's no ordering here..
// TODO: Should we try to do that? What would the ordering be?
if (method == null) {
Method[] methods = pObject.getClass().getMethods();
for (Method candidate : methods) {
if (Modifier.isPublic(candidate.getModifiers()) && candidate.getName().equals(pName)
&& candidate.getReturnType() == Void.TYPE && candidate.getParameterTypes().length == 1) {
// NOTE: Assumes paramTypes.length == 1
Class type = candidate.getParameterTypes()[0];
try {
pValues[0] = convertValueToType(pValues[0], type);
}
catch (Throwable t) {
continue;
}
// We were able to convert the parameter, let's try
method = candidate;
break;
}
}
}
// Give up...
if (method == null) {
throw e;
}
}
return method;
}
private static Object convertValueToType(Object pValue, Class> pType) throws ConversionException {
if (pType.isPrimitive()) {
if (pType == Boolean.TYPE && pValue instanceof Boolean) {
return pValue;
}
else if (pType == Byte.TYPE && pValue instanceof Byte) {
return pValue;
}
else if (pType == Character.TYPE && pValue instanceof Character) {
return pValue;
}
else if (pType == Double.TYPE && pValue instanceof Double) {
return pValue;
}
else if (pType == Float.TYPE && pValue instanceof Float) {
return pValue;
}
else if (pType == Integer.TYPE && pValue instanceof Integer) {
return pValue;
}
else if (pType == Long.TYPE && pValue instanceof Long) {
return pValue;
}
else if (pType == Short.TYPE && pValue instanceof Short) {
return pValue;
}
}
// TODO: Convert value to single-value array if needed
// TODO: Convert CSV String to string array (or potentially any type of array)
// TODO: Convert other types
if (pValue instanceof String) {
Converter converter = Converter.getInstance();
return converter.toObject((String) pValue, pType);
}
else if (pType == String.class) {
Converter converter = Converter.getInstance();
return converter.toString(pValue);
}
else {
throw new ConversionException("Cannot convert " + pValue.getClass().getName() + " to " + pType.getName());
}
}
/**
* Creates an object from the given class' single argument constructor.
*
* @param pClass The class to create instance from
* @param pParam The parameters to the constructor
*
* @return The object created from the constructor.
* If the constructor could not be invoked for any reason, null is
* returned.
*
* @throws InvocationTargetException if the constructor failed
*/
// TODO: Move to ReflectUtil
public static T createInstance(Class pClass, Object pParam)
throws InvocationTargetException {
return createInstance(pClass, new Object[] {pParam});
}
/**
* Creates an object from the given class' constructor that matches
* the given paramaters.
*
* @param pClass The class to create instance from
* @param pParams The parameters to the constructor
*
* @return The object created from the constructor.
* If the constructor could not be invoked for any reason, null is
* returned.
*
* @throws InvocationTargetException if the constructor failed
*/
// TODO: Move to ReflectUtil
public static T createInstance(Class pClass, Object... pParams)
throws InvocationTargetException {
T value;
try {
// Create param and argument arrays
Class[] paramTypes = null;
if (pParams != null && pParams.length > 0) {
paramTypes = new Class[pParams.length];
for (int i = 0; i < pParams.length; i++) {
paramTypes[i] = pParams[i].getClass();
}
}
// Get constructor
Constructor constructor = pClass.getConstructor(paramTypes);
// Invoke and create instance
value = constructor.newInstance(pParams);
}
/* All this to let InvocationTargetException pass on */
catch (NoSuchMethodException nsme) {
return null;
}
catch (IllegalAccessException iae) {
return null;
}
catch (IllegalArgumentException iarge) {
return null;
}
catch (InstantiationException ie) {
return null;
}
catch (ExceptionInInitializerError err) {
return null;
}
return value;
}
/**
* Gets an object from any given static method, with the given parameter.
*
* @param pClass The class to invoke method on
* @param pMethod The name of the method to invoke
* @param pParam The parameter to the method
*
* @return The object returned by the static method.
* If the return type of the method is a primitive type, it is wrapped in
* the corresponding wrapper object (int is wrapped in an Integer).
* If the return type of the method is void, null is returned.
* If the method could not be invoked for any reason, null is returned.
*
* @throws InvocationTargetException if the invocation failed
*/
// TODO: Move to ReflectUtil
// TODO: Rename to invokeStatic?
public static Object invokeStaticMethod(Class> pClass, String pMethod, Object pParam)
throws InvocationTargetException {
return invokeStaticMethod(pClass, pMethod, new Object[] {pParam});
}
/**
* Gets an object from any given static method, with the given parameter.
*
* @param pClass The class to invoke method on
* @param pMethod The name of the method to invoke
* @param pParams The parameters to the method
*
* @return The object returned by the static method.
* If the return type of the method is a primitive type, it is wrapped in
* the corresponding wrapper object (int is wrapped in an Integer).
* If the return type of the method is void, null is returned.
* If the method could not be invoked for any reason, null is returned.
*
* @throws InvocationTargetException if the invocation failed
*/
// TODO: Move to ReflectUtil
// TODO: Rename to invokeStatic?
public static Object invokeStaticMethod(Class> pClass, String pMethod, Object... pParams)
throws InvocationTargetException {
Object value = null;
try {
// Create param and argument arrays
Class[] paramTypes = new Class[pParams.length];
for (int i = 0; i < pParams.length; i++) {
paramTypes[i] = pParams[i].getClass();
}
// Get method
// *** If more than one such method is found in the class, and one
// of these methods has a RETURN TYPE that is more specific than
// any of the others, that method is reflected; otherwise one of
// the methods is chosen ARBITRARILY.
// java/lang/Class.html#getMethod(java.lang.String, java.lang.Class[])
Method method = pClass.getMethod(pMethod, paramTypes);
// Invoke public static method
if (Modifier.isPublic(method.getModifiers()) && Modifier.isStatic(method.getModifiers())) {
value = method.invoke(null, pParams);
}
}
/* All this to let InvocationTargetException pass on */
catch (NoSuchMethodException nsme) {
return null;
}
catch (IllegalAccessException iae) {
return null;
}
catch (IllegalArgumentException iarge) {
return null;
}
return value;
}
/**
* Configures the bean according to the given mapping.
* For each {@code Map.Entry} in {@code Map.values()},
* a method named
* {@code set + capitalize(entry.getKey())} is called on the bean,
* with {@code entry.getValue()} as its argument.
*
* Properties that has no matching set-method in the bean, are simply
* discarded.
*
*
* @param pBean The bean to configure
* @param pMapping The mapping for the bean
*
* @throws NullPointerException if any of the parameters are null.
* @throws InvocationTargetException if an error occurs when invoking the
* setter-method.
*/
// TODO: Add a version that takes a ConfigurationErrorListener callback interface
// TODO: ...or a boolean pFailOnError parameter
// TODO: ...or return Exceptions as an array?!
// TODO: ...or something whatsoever that makes clients able to determine something's not right
public static void configure(final Object pBean, final Map pMapping) throws InvocationTargetException {
configure(pBean, pMapping, false);
}
/**
* Configures the bean according to the given mapping.
* For each {@code Map.Entry} in {@code Map.values()},
* a method named
* {@code set + capitalize(entry.getKey())} is called on the bean,
* with {@code entry.getValue()} as its argument.
*
* Optionally, lisp-style names are allowed, and automatically converted
* to Java-style camel-case names.
*
*
* Properties that has no matching set-method in the bean, are simply
* discarded.
*
*
* @see StringUtil#lispToCamel(String)
*
* @param pBean The bean to configure
* @param pMapping The mapping for the bean
* @param pLispToCamel Allow lisp-style names, and automatically convert
* them to Java-style camel-case.
*
* @throws NullPointerException if any of the parameters are null.
* @throws InvocationTargetException if an error occurs when invoking the
* setter-method.
*/
public static void configure(final Object pBean, final Map pMapping, final boolean pLispToCamel) throws InvocationTargetException {
// Loop over properties in mapping
for (final Map.Entry entry : pMapping.entrySet()) {
try {
// Configure each property in turn
final String property = StringUtil.valueOf(entry.getKey());
try {
setPropertyValue(pBean, property, entry.getValue());
}
catch (NoSuchMethodException ignore) {
// If invocation failed, convert lisp-style and try again
if (pLispToCamel && property.indexOf('-') > 0) {
setPropertyValue(pBean, StringUtil.lispToCamel(property, false), entry.getValue());
}
}
}
catch (NoSuchMethodException nsme) {
// This property was not configured
}
catch (IllegalAccessException iae) {
// This property was not configured
}
}
}
}