File: BeanProperty.java

package info (click to toggle)
libglazedlists-java 1.9.0%2Bdfsg-1
  • links: PTS, VCS
  • area: main
  • in suites: jessie, jessie-kfreebsd
  • size: 3,024 kB
  • ctags: 4,252
  • sloc: java: 22,561; xml: 818; sh: 51; makefile: 5
file content (368 lines) | stat: -rw-r--r-- 13,946 bytes parent folder | download | duplicates (3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
/* Glazed Lists                                                 (c) 2003-2006 */
/* http://publicobject.com/glazedlists/                      publicobject.com,*/
/*                                                     O'Dell Engineering Ltd.*/
package ca.odell.glazedlists.impl.beans;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.UndeclaredThrowableException;
import java.util.ArrayList;
import java.util.List;

import ca.odell.glazedlists.impl.reflect.J2SE50ReturnTypeResolver;
import ca.odell.glazedlists.impl.reflect.ReturnTypeResolver;

/**
 * Models a getter and setter for an abstract property.
 *
 * @author <a href="mailto:jesse@swank.ca">Jesse Wilson</a>
 */
public class BeanProperty<T> {

    private static final ReturnTypeResolver TYPE_RESOLVER = new J2SE50ReturnTypeResolver();

    /** the target class */
    private final Class<T> beanClass;
    /** the property name */
    private final String propertyName;

    /** <tt>true</tt> indicates the getter should simply reflect the value it is given */
    private final boolean identityProperty;

    /** the value class */
    private Class valueClass = null;

    /** the chain of methods for the getter */
    private List<Method> getterChain = null;

    /** the chain of methods for the setter */
    private List<Method> setterChain = null;

    /** commonly used paramters */
    private static final Object[] EMPTY_ARGUMENTS = new Object[0];
    private static final Class[] EMPTY_PARAMETER_TYPES = new Class[0];

    /**
     * Creates a new {@link BeanProperty} that gets the specified property from the
     * specified class.
     */
    public BeanProperty(Class<T> beanClass, String propertyName, boolean readable, boolean writable) {
        if (beanClass == null)
            throw new IllegalArgumentException("beanClass may not be null");
        if (propertyName == null)
            throw new IllegalArgumentException("propertyName may not be null");
        if (propertyName.length() == 0)
            throw new IllegalArgumentException("propertyName may not be empty");

        this.beanClass = beanClass;
        this.propertyName = propertyName;
        this.identityProperty = "this".equals(propertyName);

        if (identityProperty && writable)
            throw new IllegalArgumentException("The identity property name (this) cannot be writable");

        // look up the common chain
        final String[] propertyParts = propertyName.split("\\.");
        final List<Method> commonChain = new ArrayList<Method>(propertyParts.length);
        Class currentClass = beanClass;
        for(int p = 0; p < propertyParts.length - 1; p++) {
            Method partGetter = findGetterMethod(currentClass, propertyParts[p]);
            commonChain.add(partGetter);
            currentClass = TYPE_RESOLVER.getReturnType(currentClass, partGetter);
        }

        // look up the final getter
        if(readable) {
            if (identityProperty) {
                valueClass = beanClass;
            } else {
                getterChain = new ArrayList<Method>();
                getterChain.addAll(commonChain);
                Method lastGetter = findGetterMethod(currentClass, propertyParts[propertyParts.length - 1]);
                getterChain.add(lastGetter);
                valueClass = TYPE_RESOLVER.getReturnType(currentClass, lastGetter);
            }
        }

        // look up the final setter
        if(writable) {
            setterChain = new ArrayList<Method>();
            setterChain.addAll(commonChain);
            Method lastSetter = findSetterMethod(currentClass, propertyParts[propertyParts.length - 1]);
            setterChain.add(lastSetter);
            if(valueClass == null) valueClass = TYPE_RESOLVER.getFirstParameterType(currentClass, lastSetter);
        }
    }

    /**
     * Finds a getter of the specified property on the specified class.
     */
    private Method findGetterMethod(Class targetClass, String property) {
        Method result;

        Class currentClass = targetClass;
        while(currentClass != null) {
            String getProperty = "get" + capitalize(property);
            result = getMethod(currentClass, getProperty, EMPTY_PARAMETER_TYPES);
            if(result != null) {
                validateGetter(result);
                return result;
            }

            String isProperty = "is" + capitalize(property);
            result = getMethod(currentClass, isProperty, EMPTY_PARAMETER_TYPES);
            if(result != null) {
                validateGetter(result);
                return result;
            }
            currentClass = currentClass.getSuperclass();
        }

        throw new IllegalArgumentException("Failed to find getter for property \"" + property + "\" of " + targetClass);
    }

    /**
     * Finds a setter of the specified property on the specified class.
     */
    private Method findSetterMethod(Class targetClass, String property) {
        String setProperty = "set" + capitalize(property);

        // loop through the class and its superclasses
        Class currentClass = targetClass;
        while(currentClass != null) {

            // loop through this class' methods
            Method[] classMethods = currentClass.getMethods();
            for(int m = 0; m < classMethods.length; m++) {
                if(!classMethods[m].getName().equals(setProperty)) continue;
                if(classMethods[m].getParameterTypes().length != 1) continue;
                validateSetter(classMethods[m]);
                return classMethods[m];
            }
            currentClass = currentClass.getSuperclass();
        }

        throw new IllegalArgumentException("Failed to find setter for property \"" + property + "\" of " + targetClass);
    }

    /**
     * Validates that the specified method is okay for reflection. This throws an
     * exception if the method is invalid.
     */
    private void validateGetter(Method method) {
        if(!Modifier.isPublic(method.getModifiers())) {
            throw new IllegalArgumentException("Getter \"" + method + "\" is not public");
        }

        if(Void.TYPE.equals(method.getReturnType())) {
            throw new IllegalArgumentException("Getter \"" + method + "\" returns void");
        }

        if(method.getParameterTypes().length != 0) {
            throw new IllegalArgumentException("Getter \"" + method + "\" has too many parameters; expected 0 but found " + method.getParameterTypes().length);
        }
    }

    /**
     * Validates that the specified method is okay for reflection. This throws an
     * exception if the method is invalid.
     */
    private void validateSetter(Method method) {
        if(!Modifier.isPublic(method.getModifiers())) {
            throw new IllegalArgumentException("Setter \"" + method + "\" is not public");
        }

        if(method.getParameterTypes().length != 1) {
            throw new IllegalArgumentException("Setter \"" + method + "\" takes the wrong number of parameters; expected 1 but found " + method.getParameterTypes().length);
        }
    }


    /**
     * Returns the specified property with a capitalized first character.
     */
    private String capitalize(String property) {
        StringBuffer result = new StringBuffer();
        result.append(Character.toUpperCase(property.charAt(0)));
        result.append(property.substring(1));
        return result.toString();
    }

    /**
     * Gets the method with the specified name and arguments.
     */
    private Method getMethod(Class targetClass, String methodName, Class[] parameterTypes) {
        try {
            return targetClass.getMethod(methodName, parameterTypes);
        } catch(NoSuchMethodException e) {
            return null;
        }
    }

    /**
     * Gets the base class that this getter accesses.
     */
    public Class<T> getBeanClass() {
         return beanClass;
    }

    /**
     * Gets the name of the property that this getter extracts.
     */
    public String getPropertyName() {
         return propertyName;
    }

    /**
     * Gets the class of the property's value. This is the return type and not
     * necessarily the runtime type of the class.
     */
    public Class getValueClass() {
        return valueClass;
    }

    /**
     * Gets whether this property can get get.
     */
    public boolean isReadable() {
        return getterChain != null || identityProperty;
    }

    /**
     * Gets whether this property can be set.
     */
    public boolean isWritable() {
        return setterChain != null;
    }

    /**
     * Gets the value of this property for the specified Object.
     */
    public Object get(T member) {
        if(!isReadable()) throw new IllegalStateException("Property " + propertyName + " of " + beanClass + " not readable");

        // the identity property simply reflects the member back unchanged
        if (identityProperty)
            return member;

        try {
            // do all the getters in sequence
            Object currentMember = member;
            for(int i = 0, n = getterChain.size(); i < n; i++) {
                Method currentMethod = getterChain.get(i);
                currentMember = currentMethod.invoke(currentMember, EMPTY_ARGUMENTS);
                if(currentMember == null) return null;
            }

            // return the result of the last getter
            return currentMember;
        } catch(IllegalAccessException e) {
            SecurityException se = new SecurityException();
            se.initCause(e);
            throw se;
        } catch(InvocationTargetException e) {
            throw new UndeclaredThrowableException(e.getCause());
        }
    }

    /**
     * Gets the value of this property for the specified Object.
     */
    public Object set(T member, Object newValue) {
        if(!isWritable()) throw new IllegalStateException("Property " + propertyName + " of " + beanClass + " not writable");

        Method setterMethod = null;
        try {
            // everything except the last setter chain element is a getter
            Object currentMember = member;
            for(int i = 0, n = setterChain.size() - 1; i < n; i++) {
                Method currentMethod = setterChain.get(i);
                currentMember = currentMethod.invoke(currentMember, EMPTY_ARGUMENTS);
                if(currentMember == null) return null;
            }

            // do the remaining setter
            setterMethod = setterChain.get(setterChain.size() - 1);
            return setterMethod.invoke(currentMember, new Object[] { newValue });
        } catch (IllegalArgumentException e) {
            String message = e.getMessage();

            // improve the message if possible into something like:
            // "MyClass.someMethod(SomeClassType) cannot be called with an instance of WrongClassType"
            if ("argument type mismatch".equals(message) && setterMethod != null)
                message = getSimpleName(setterMethod.getDeclaringClass()) + "." + setterMethod.getName() + "(" + getSimpleName(setterMethod.getParameterTypes()[0]) + ") cannot be called with an instance of " + getSimpleName(newValue.getClass());

            throw new IllegalArgumentException(message);
        } catch(IllegalAccessException e) {
            SecurityException se = new SecurityException();
            se.initCause(e);
            throw se;
        } catch(InvocationTargetException e) {
            throw new UndeclaredThrowableException(e.getCause());
        } catch(RuntimeException e) {
            throw new RuntimeException("Failed to set property \"" + propertyName + "\" of " + beanClass + " to " + (newValue == null ? "null" : "instance of " + newValue.getClass()), e);
        }
    }

    /**
     * This method was backported from the JDK 1.5 version of java.lang.Class.
     *
     * Returns the simple name of the given class as given in the
     * source code. Returns an empty string if the underlying class is
     * anonymous.
     *
     * @return the simple name of the given class
     */
    private static String getSimpleName(Class clazz) {
        Class declaringClass = clazz.getDeclaringClass();
        String simpleName = declaringClass == null ? null : declaringClass.getName();
        if (simpleName == null) { // top level class
            simpleName = clazz.getName();
            return simpleName.substring(simpleName.lastIndexOf(".") + 1); // strip the package name
        }

        // Remove leading "\$[0-9]*" from the name
        int length = simpleName.length();
        if (length < 1 || simpleName.charAt(0) != '$')
            throw new InternalError("Malformed class name");
        int index = 1;
        while (index < length && isAsciiDigit(simpleName.charAt(index)))
            index++;
        // Eventually, this is the empty string iff this is an anonymous class
        return simpleName.substring(index);
    }

    /**
     * This method was backported from the JDK 1.5 version of java.lang.Class.
     *
     * Character.isDigit answers <tt>true</tt> to some non-ascii
     * digits. This one does not.
     */
    private static boolean isAsciiDigit(char c) {
        return '0' <= c && c <= '9';
    }

    /** {@inheritDoc} */
    @Override
    public boolean equals(Object o) {
        if(this == o) return true;
        if(o == null || getClass() != o.getClass()) return false;

        final BeanProperty that = (BeanProperty) o;

        if(!beanClass.equals(that.beanClass)) return false;
        if(!propertyName.equals(that.propertyName)) return false;

        return true;
    }

    /** {@inheritDoc} */
    @Override
    public int hashCode() {
        int result;
        result = beanClass.hashCode();
        result = 29 * result + propertyName.hashCode();
        return result;
    }
}