1/*
2 * Copyright (C) 2011 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16package android.util;
17
18import java.lang.reflect.Field;
19import java.lang.reflect.InvocationTargetException;
20import java.lang.reflect.Method;
21
22/**
23 * Internal class to automatically generate a Property for a given class/name pair, given the
24 * specification of {@link Property#of(java.lang.Class, java.lang.Class, java.lang.String)}
25 */
26class ReflectiveProperty<T, V> extends Property<T, V> {
27
28    private static final String PREFIX_GET = "get";
29    private static final String PREFIX_IS = "is";
30    private static final String PREFIX_SET = "set";
31    private Method mSetter;
32    private Method mGetter;
33    private Field mField;
34
35    /**
36     * For given property name 'name', look for getName/isName method or 'name' field.
37     * Also look for setName method (optional - could be readonly). Failing method getters and
38     * field results in throwing NoSuchPropertyException.
39     *
40     * @param propertyHolder The class on which the methods or field are found
41     * @param name The name of the property, where this name is capitalized and appended to
42     * "get" and "is to search for the appropriate methods. If the get/is methods are not found,
43     * the constructor will search for a field with that exact name.
44     */
45    public ReflectiveProperty(Class<T> propertyHolder, Class<V> valueType, String name) {
46         // TODO: cache reflection info for each new class/name pair
47        super(valueType, name);
48        char firstLetter = Character.toUpperCase(name.charAt(0));
49        String theRest = name.substring(1);
50        String capitalizedName = firstLetter + theRest;
51        String getterName = PREFIX_GET + capitalizedName;
52        try {
53            mGetter = propertyHolder.getMethod(getterName, (Class<?>[])null);
54        } catch (NoSuchMethodException e) {
55            // getName() not available - try isName() instead
56            getterName = PREFIX_IS + capitalizedName;
57            try {
58                mGetter = propertyHolder.getMethod(getterName, (Class<?>[])null);
59            } catch (NoSuchMethodException e1) {
60                // Try public field instead
61                try {
62                    mField = propertyHolder.getField(name);
63                    Class fieldType = mField.getType();
64                    if (!typesMatch(valueType, fieldType)) {
65                        throw new NoSuchPropertyException("Underlying type (" + fieldType + ") " +
66                                "does not match Property type (" + valueType + ")");
67                    }
68                    return;
69                } catch (NoSuchFieldException e2) {
70                    // no way to access property - throw appropriate exception
71                    throw new NoSuchPropertyException("No accessor method or field found for"
72                            + " property with name " + name);
73                }
74            }
75        }
76        Class getterType = mGetter.getReturnType();
77        // Check to make sure our getter type matches our valueType
78        if (!typesMatch(valueType, getterType)) {
79            throw new NoSuchPropertyException("Underlying type (" + getterType + ") " +
80                    "does not match Property type (" + valueType + ")");
81        }
82        String setterName = PREFIX_SET + capitalizedName;
83        try {
84            mSetter = propertyHolder.getMethod(setterName, getterType);
85        } catch (NoSuchMethodException ignored) {
86            // Okay to not have a setter - just a readonly property
87        }
88    }
89
90    /**
91     * Utility method to check whether the type of the underlying field/method on the target
92     * object matches the type of the Property. The extra checks for primitive types are because
93     * generics will force the Property type to be a class, whereas the type of the underlying
94     * method/field will probably be a primitive type instead. Accept float as matching Float,
95     * etc.
96     */
97    private boolean typesMatch(Class<V> valueType, Class getterType) {
98        if (getterType != valueType) {
99            if (getterType.isPrimitive()) {
100                return (getterType == float.class && valueType == Float.class) ||
101                        (getterType == int.class && valueType == Integer.class) ||
102                        (getterType == boolean.class && valueType == Boolean.class) ||
103                        (getterType == long.class && valueType == Long.class) ||
104                        (getterType == double.class && valueType == Double.class) ||
105                        (getterType == short.class && valueType == Short.class) ||
106                        (getterType == byte.class && valueType == Byte.class) ||
107                        (getterType == char.class && valueType == Character.class);
108            }
109            return false;
110        }
111        return true;
112    }
113
114    @Override
115    public void set(T object, V value) {
116        if (mSetter != null) {
117            try {
118                mSetter.invoke(object, value);
119            } catch (IllegalAccessException e) {
120                throw new AssertionError();
121            } catch (InvocationTargetException e) {
122                throw new RuntimeException(e.getCause());
123            }
124        } else if (mField != null) {
125            try {
126                mField.set(object, value);
127            } catch (IllegalAccessException e) {
128                throw new AssertionError();
129            }
130        } else {
131            throw new UnsupportedOperationException("Property " + getName() +" is read-only");
132        }
133    }
134
135    @Override
136    public V get(T object) {
137        if (mGetter != null) {
138            try {
139                return (V) mGetter.invoke(object, (Object[])null);
140            } catch (IllegalAccessException e) {
141                throw new AssertionError();
142            } catch (InvocationTargetException e) {
143                throw new RuntimeException(e.getCause());
144            }
145        } else if (mField != null) {
146            try {
147                return (V) mField.get(object);
148            } catch (IllegalAccessException e) {
149                throw new AssertionError();
150            }
151        }
152        // Should not get here: there should always be a non-null getter or field
153        throw new AssertionError();
154    }
155
156    /**
157     * Returns false if there is no setter or public field underlying this Property.
158     */
159    @Override
160    public boolean isReadOnly() {
161        return (mSetter == null && mField == null);
162    }
163}
164