com.rt.storage.api.client.util.FieldInfo Maven / Gradle / Ivy
package com.rt.storage.api.client.util;
import com.google.common.base.Ascii;
import com.rt.storage.api.client.util.Data;
import com.rt.storage.api.client.util.NullValue;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.WeakHashMap;
/**
* Parses field information to determine data key name/value pair associated with the field.
*
* Implementation is thread-safe.
*
* @since 1.0
* @author Yaniv Inbar
*/
public class FieldInfo {
/** Cached field information. */
private static final Map CACHE = new WeakHashMap();
/**
* Returns the field information for the given enum value.
*
* @param enumValue enum value
* @return field information
* @throws IllegalArgumentException if the enum value has no value annotation
* @since 1.4
*/
public static FieldInfo of(Enum> enumValue) {
try {
FieldInfo result = FieldInfo.of(enumValue.getClass().getField(enumValue.name()));
Preconditions.checkArgument(
result != null, "enum constant missing @Value or @NullValue annotation: %s", enumValue);
return result;
} catch (NoSuchFieldException e) {
// not possible
throw new RuntimeException(e);
}
}
/**
* Returns the field information for the given field.
*
* @param field field or {@code null} for {@code null} result
* @return field information or {@code null} if the field has no {@link #name} or for {@code null}
* input
*/
public static FieldInfo of(Field field) {
if (field == null) {
return null;
}
synchronized (CACHE) {
FieldInfo fieldInfo = CACHE.get(field);
boolean isEnumContant = field.isEnumConstant();
if (fieldInfo == null && (isEnumContant || !Modifier.isStatic(field.getModifiers()))) {
String fieldName;
if (isEnumContant) {
// check for @Value annotation
Value value = field.getAnnotation(Value.class);
if (value != null) {
fieldName = value.value();
} else {
// check for @NullValue annotation
NullValue nullValue = field.getAnnotation(NullValue.class);
if (nullValue != null) {
fieldName = null;
} else {
// else ignore
return null;
}
}
} else {
// check for @Key annotation
Key key = field.getAnnotation(Key.class);
if (key == null) {
// else ignore
return null;
}
fieldName = key.value();
field.setAccessible(true);
}
if ("##default".equals(fieldName)) {
fieldName = field.getName();
}
fieldInfo = new FieldInfo(field, fieldName);
CACHE.put(field, fieldInfo);
}
return fieldInfo;
}
}
/** Whether the field class is "primitive" as defined by {@link Data#isPrimitive(Type)}. */
private final boolean isPrimitive;
/** Field. */
private final Field field;
private final Method[] setters;
/**
* Data key name associated with the field for a non-enum-constant with a {@link Key} annotation,
* or data key value associated with the enum constant with a {@link Value} annotation or {@code
* null} for an enum constant with a {@link NullValue} annotation.
*
* This string is interned.
*/
private final String name;
FieldInfo(Field field, String name) {
this.field = field;
this.name = name == null ? null : name.intern();
isPrimitive = Data.isPrimitive(getType());
this.setters = settersMethodForField(field);
}
/** Creates list of setter methods for a field only in declaring class. */
private Method[] settersMethodForField(Field field) {
List methods = new ArrayList<>();
for (Method method : field.getDeclaringClass().getDeclaredMethods()) {
if (Ascii.toLowerCase(method.getName()).equals("set" + Ascii.toLowerCase(field.getName()))
&& method.getParameterTypes().length == 1) {
methods.add(method);
}
}
return methods.toArray(new Method[0]);
}
/**
* Returns the field.
*
* @since 1.4
*/
public Field getField() {
return field;
}
/**
* Returns the data key name associated with the field for a non-enum-constant with a {@link Key}
* annotation, or data key value associated with the enum constant with a {@link Value} annotation
* or {@code null} for an enum constant with a {@link NullValue} annotation.
*
* This string is interned.
*
* @since 1.4
*/
public String getName() {
return name;
}
/**
* Returns the field's type.
*
* @since 1.4
*/
public Class> getType() {
return field.getType();
}
/**
* Returns the field's generic type, which is a class, parameterized type, generic array type, or
* type variable, but not a wildcard type.
*
* @since 1.4
*/
public Type getGenericType() {
return field.getGenericType();
}
/**
* Returns whether the field is final.
*
* @since 1.4
*/
public boolean isFinal() {
return Modifier.isFinal(field.getModifiers());
}
/**
* Returns whether the field is primitive as defined by {@link Data#isPrimitive(Type)}.
*
* @since 1.4
*/
public boolean isPrimitive() {
return isPrimitive;
}
/** Returns the value of the field in the given object instance using reflection. */
public Object getValue(Object obj) {
return getFieldValue(field, obj);
}
/**
* Sets this field in the given object to the given value using reflection.
*
*
If the field is final, it checks that the value being set is identical to the existing
* value.
*/
public void setValue(Object obj, Object value) {
if (setters.length > 0) {
for (Method method : setters) {
if (value == null || method.getParameterTypes()[0].isAssignableFrom(value.getClass())) {
try {
method.invoke(obj, value);
return;
} catch (IllegalAccessException | InvocationTargetException e) {
// try to set field directly
}
}
}
}
setFieldValue(field, obj, value);
}
/** Returns the class information of the field's declaring class. */
public ClassInfo getClassInfo() {
return ClassInfo.of(field.getDeclaringClass());
}
@SuppressWarnings("unchecked")
public > T enumValue() {
return Enum.valueOf((Class) field.getDeclaringClass(), field.getName());
}
/** Returns the value of the given field in the given object using reflection. */
public static Object getFieldValue(Field field, Object obj) {
try {
return field.get(obj);
} catch (IllegalAccessException e) {
throw new IllegalArgumentException(e);
}
}
/**
* Sets the given field in the given object to the given value using reflection.
*
* If the field is final, it checks that the value being set is identical to the existing
* value.
*/
public static void setFieldValue(Field field, Object obj, Object value) {
if (Modifier.isFinal(field.getModifiers())) {
Object finalValue = getFieldValue(field, obj);
if (value == null ? finalValue != null : !value.equals(finalValue)) {
throw new IllegalArgumentException(
"expected final value <"
+ finalValue
+ "> but was <"
+ value
+ "> on "
+ field.getName()
+ " field in "
+ obj.getClass().getName());
}
} else {
try {
field.set(obj, value);
} catch (SecurityException e) {
throw new IllegalArgumentException(e);
} catch (IllegalAccessException e) {
throw new IllegalArgumentException(e);
}
}
}
}