com.nineoldandroids.util.ReflectiveProperty Maven / Gradle / Ivy
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.nineoldandroids.util;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
/**
* Internal class to automatically generate a Property for a given class/name pair, given the
* specification of {@link Property#of(java.lang.Class, java.lang.Class, java.lang.String)}
*/
class ReflectiveProperty extends Property {
private static final String PREFIX_GET = "get";
private static final String PREFIX_IS = "is";
private static final String PREFIX_SET = "set";
private Method mSetter;
private Method mGetter;
private Field mField;
/**
* For given property name 'name', look for getName/isName method or 'name' field.
* Also look for setName method (optional - could be readonly). Failing method getters and
* field results in throwing NoSuchPropertyException.
*
* @param propertyHolder The class on which the methods or field are found
* @param name The name of the property, where this name is capitalized and appended to
* "get" and "is to search for the appropriate methods. If the get/is methods are not found,
* the constructor will search for a field with that exact name.
*/
public ReflectiveProperty(Class propertyHolder, Class valueType, String name) {
// TODO: cache reflection info for each new class/name pair
super(valueType, name);
char firstLetter = Character.toUpperCase(name.charAt(0));
String theRest = name.substring(1);
String capitalizedName = firstLetter + theRest;
String getterName = PREFIX_GET + capitalizedName;
try {
mGetter = propertyHolder.getMethod(getterName, (Class>[])null);
} catch (NoSuchMethodException e) {
// getName() not available - try isName() instead
getterName = PREFIX_IS + capitalizedName;
try {
mGetter = propertyHolder.getMethod(getterName, (Class>[])null);
} catch (NoSuchMethodException e1) {
// Try public field instead
try {
mField = propertyHolder.getField(name);
Class fieldType = mField.getType();
if (!typesMatch(valueType, fieldType)) {
throw new NoSuchPropertyException("Underlying type (" + fieldType + ") " +
"does not match Property type (" + valueType + ")");
}
return;
} catch (NoSuchFieldException e2) {
// no way to access property - throw appropriate exception
throw new NoSuchPropertyException("No accessor method or field found for"
+ " property with name " + name);
}
}
}
Class getterType = mGetter.getReturnType();
// Check to make sure our getter type matches our valueType
if (!typesMatch(valueType, getterType)) {
throw new NoSuchPropertyException("Underlying type (" + getterType + ") " +
"does not match Property type (" + valueType + ")");
}
String setterName = PREFIX_SET + capitalizedName;
try {
mSetter = propertyHolder.getMethod(setterName, getterType);
} catch (NoSuchMethodException ignored) {
// Okay to not have a setter - just a readonly property
}
}
/**
* Utility method to check whether the type of the underlying field/method on the target
* object matches the type of the Property. The extra checks for primitive types are because
* generics will force the Property type to be a class, whereas the type of the underlying
* method/field will probably be a primitive type instead. Accept float as matching Float,
* etc.
*/
private boolean typesMatch(Class valueType, Class getterType) {
if (getterType != valueType) {
if (getterType.isPrimitive()) {
return (getterType == float.class && valueType == Float.class) ||
(getterType == int.class && valueType == Integer.class) ||
(getterType == boolean.class && valueType == Boolean.class) ||
(getterType == long.class && valueType == Long.class) ||
(getterType == double.class && valueType == Double.class) ||
(getterType == short.class && valueType == Short.class) ||
(getterType == byte.class && valueType == Byte.class) ||
(getterType == char.class && valueType == Character.class);
}
return false;
}
return true;
}
@Override
public void set(T object, V value) {
if (mSetter != null) {
try {
mSetter.invoke(object, value);
} catch (IllegalAccessException e) {
throw new AssertionError();
} catch (InvocationTargetException e) {
throw new RuntimeException(e.getCause());
}
} else if (mField != null) {
try {
mField.set(object, value);
} catch (IllegalAccessException e) {
throw new AssertionError();
}
} else {
throw new UnsupportedOperationException("Property " + getName() +" is read-only");
}
}
@Override
public V get(T object) {
if (mGetter != null) {
try {
return (V) mGetter.invoke(object, (Object[])null);
} catch (IllegalAccessException e) {
throw new AssertionError();
} catch (InvocationTargetException e) {
throw new RuntimeException(e.getCause());
}
} else if (mField != null) {
try {
return (V) mField.get(object);
} catch (IllegalAccessException e) {
throw new AssertionError();
}
}
// Should not get here: there should always be a non-null getter or field
throw new AssertionError();
}
/**
* Returns false if there is no setter or public field underlying this Property.
*/
@Override
public boolean isReadOnly() {
return (mSetter == null && mField == null);
}
}