package io.katharsis.utils;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
/**
* <p>
* A lighter version of Apache Commons PropertyUtils without additional dependencies and with support for fluent
* setters.
* </p>
*/
public class PropertyUtils {
private static final PropertyUtils INSTANCE = new PropertyUtils();
private PropertyUtils() {
}
/**
* Get bean's property value. The sequence of searches for getting a value is as follows:
* <ol>
* <li>All class fields are found using {@link ClassUtils#getClassFields(Class)}</li>
* <li>Search for a field with the name of the desired one is made</li>
* <li>If a field is found and it's a non-public field, the value is returned using the accompanying getter</li>
* <li>If a field is found and it's a public field, the value is returned using the public field</li>
* <li>If a field is not found, a search for a getter is made - all class getters are found using
* {@link ClassUtils#getClassFields(Class)}</li>
* <li>From class getters, an appropriate getter with name of the desired one is used</li>
* </ol>
*
* @param bean bean to be accessed
* @param field bean's fieldName
* @return bean's property value
*/
public static Object getProperty(Object bean, String field) {
INSTANCE.checkParameters(bean, field);
try {
return INSTANCE.getPropertyValue(bean, field);
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
throw handleReflectionException(bean, field, e);
}
}
private void checkParameters(Object bean, String field) {
if (bean == null) {
throw new IllegalArgumentException("No bean specified");
}
if (field == null) {
throw new IllegalArgumentException(String.format("No field specified for bean: %s", bean.getClass()));
}
}
private Object getPropertyValue(Object bean, String fieldName)
throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
Field foundField = findField(bean, fieldName);
if (foundField != null) {
if (!Modifier.isPublic(foundField.getModifiers())) {
Method getter = getGetter(bean, foundField.getName());
return getter.invoke(bean);
} else {
return foundField.get(bean);
}
} else {
Method getter = findGetter(bean, fieldName);
if (getter == null) {
String message = String
.format("Cannot find an getter for %s.%s", bean.getClass().getCanonicalName(), fieldName);
throw new PropertyException(message, bean.getClass(), fieldName);
}
return getter.invoke(bean);
}
}
private Method findGetter(Object bean, String fieldName) {
List<Method> classGetters = ClassUtils.getClassGetters(bean.getClass());
for (Method getter : classGetters) {
String getterFieldName = getGetterFieldName(getter);
if (getterFieldName.equals(fieldName)) {
return getter;
}
}
return null;
}
private String getGetterFieldName(Method getter) {
if (isBoolean(getter.getReturnType())) {
return getter.getName().substring(2, 3).toLowerCase() + getter.getName().substring(3);
} else {
return getter.getName().substring(3, 4).toLowerCase() + getter.getName().substring(4);
}
}
private boolean isBoolean(Class<?> returnType) {
return boolean.class.equals(returnType) || Boolean.class.equals(returnType);
}
private Field findField(Object bean, String fieldName) {
List<Field> classFields = ClassUtils.getClassFields(bean.getClass());
for (Field field : classFields) {
if (field.getName().equals(fieldName)) {
return field;
}
}
return null;
}
private Method getGetter(Object bean, String fieldName) throws NoSuchMethodException {
Class<?> beanClass = bean.getClass();
String upperCaseName = fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1);
try {
return beanClass.getMethod("get" + upperCaseName);
} catch (NoSuchMethodException e) {
return beanClass.getMethod("is" + upperCaseName);
}
}
/**
* Set bean's property value. The sequence of searches for setting a value is as follows:
* <ol>
* <li>All class fields are found using {@link ClassUtils#getClassFields(Class)}</li>
* <li>Search for a field with the name of the desired one is made</li>
* <li>If a field is found and it's a non-public field, the value is assigned using the accompanying setter</li>
* <li>If a field is found and it's a public field, the value is assigned using the public field</li>
* <li>If a field is not found, a search for a getter is made - all class getters are found using
* {@link ClassUtils#getClassFields(Class)}</li>
* <li>From class getters, an appropriate getter with name of the desired one is searched</li>
* <li>Using the found getter, an accompanying setter is being used to assign the value</li>
* </ol>
* <p>
* <b>Important</b>
* </p>
* <ul>
* <li>Each setter should have accompanying getter.</li>
* <li>If a value to be set is of type {@link List} and the property type is {@link Set}, the collection is changed to {@link Set}</li>
* <li>If a value to be set is of type {@link Set} and the property type is {@link List}, the collection is changed to {@link List}</li>
* </ul>
*
* @param bean bean to be accessed
* @param field bean's fieldName
* @param value value to be set
*/
public static void setProperty(Object bean, String field, Object value) {
INSTANCE.checkParameters(bean, field);
try {
INSTANCE.setPropertyValue(bean, field, value);
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
throw handleReflectionException(bean, field, e);
}
}
private static RuntimeException handleReflectionException(Object bean, String field, ReflectiveOperationException e) {
if (e instanceof InvocationTargetException &&
((InvocationTargetException) e).getTargetException() instanceof RuntimeException) {
return (RuntimeException) ((InvocationTargetException) e).getTargetException();
}
return new PropertyException(e, bean.getClass(), field);
}
private void setPropertyValue(Object bean, String fieldName, Object value)
throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
Field foundField = findField(bean, fieldName);
if (foundField != null) {
if ( !Modifier.isPublic(foundField.getModifiers())) {
Method setter = getSetter(bean, foundField.getName(), foundField.getType());
setter.invoke(bean, prepareValue(value, setter.getParameterTypes()[0]));
} else {
foundField.set(bean, prepareValue(value, foundField.getType()));
}
} else {
Method getter = findGetter(bean, fieldName);
if (getter == null) {
String message = String.format("Cannot find a getter for %s.%s", bean.getClass().getCanonicalName(), fieldName);
throw new PropertyException(message, bean.getClass(), fieldName);
}
String getterFieldName = getGetterFieldName(getter);
Method setter = getSetter(bean, getterFieldName, getter.getReturnType());
setter.invoke(bean, prepareValue(value, setter.getParameterTypes()[0]));
}
}
@SuppressWarnings("unchecked")
private Object prepareValue(Object value, Class<?> fieldClass) {
if (Set.class.isAssignableFrom(fieldClass) && value instanceof List) {
List listValue = (List) value;
Set setValue = new HashSet<>(listValue.size());
setValue.addAll(listValue);
return setValue;
} else if (List.class.isAssignableFrom(fieldClass) && value instanceof Set) {
return new LinkedList<>((Set)value);
}
return value;
}
private Method getSetter(Object bean, String fieldName, Class<?> fieldType) throws NoSuchMethodException {
Class<?> beanClass = bean.getClass();
String upperCaseName = fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1);
return beanClass.getMethod("set" + upperCaseName, fieldType);
}
}