001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017
018package org.apache.commons.beanutils;
019
020import java.beans.IntrospectionException;
021import java.beans.PropertyDescriptor;
022import java.lang.ref.Reference;
023import java.lang.ref.SoftReference;
024import java.lang.ref.WeakReference;
025import java.lang.reflect.Method;
026import java.lang.reflect.Modifier;
027
028/**
029 * A MappedPropertyDescriptor describes one mapped property.
030 * Mapped properties are multivalued properties like indexed properties
031 * but that are accessed with a String key instead of an index.
032 * Such property values are typically stored in a Map collection.
033 * For this class to work properly, a mapped value must have
034 * getter and setter methods of the form
035 * <p><code>get<strong>Property</strong>(String key)</code> and
036 * <p><code>set<strong>Property</strong>(String key, Object value)</code>,
037 * <p>where <code><strong>Property</strong></code> must be replaced
038 * by the name of the property.
039 * @see java.beans.PropertyDescriptor
040 *
041 */
042public class MappedPropertyDescriptor extends PropertyDescriptor {
043
044    /**
045     * Holds a {@link Method} in a {@link SoftReference} so that it
046     * it doesn't prevent any ClassLoader being garbage collected, but
047     * tries to re-create the method if the method reference has been
048     * released.
049     *
050     * See http://issues.apache.org/jira/browse/BEANUTILS-291
051     */
052    private static class MappedMethodReference {
053        private String className;
054        private String methodName;
055        private Reference<Method> methodRef;
056        private Reference<Class<?>> classRef;
057        private Reference<Class<?>> writeParamTypeRef0;
058        private Reference<Class<?>> writeParamTypeRef1;
059        private String[] writeParamClassNames;
060        MappedMethodReference(final Method m) {
061            if (m != null) {
062                className = m.getDeclaringClass().getName();
063                methodName = m.getName();
064                // Compiler needs generic.
065                methodRef = new SoftReference<>(m);
066                // Compiler needs generic.
067                classRef = new WeakReference<>(m.getDeclaringClass());
068                final Class<?>[] types = m.getParameterTypes();
069                if (types.length == 2) {
070                    // Compiler needs generic.
071                    writeParamTypeRef0 = new WeakReference<>(types[0]);
072                    // Compiler needs generic.
073                    writeParamTypeRef1 = new WeakReference<>(types[1]);
074                    writeParamClassNames = new String[2];
075                    writeParamClassNames[0] = types[0].getName();
076                    writeParamClassNames[1] = types[1].getName();
077                }
078            }
079        }
080        private Method get() {
081            if (methodRef == null) {
082                return null;
083            }
084            Method m = methodRef.get();
085            if (m == null) {
086                Class<?> clazz = classRef.get();
087                if (clazz == null) {
088                    clazz = reLoadClass();
089                    if (clazz != null) {
090                        // Compiler needs generic.
091                        classRef = new WeakReference<>(clazz);
092                    }
093                }
094                if (clazz == null) {
095                    throw new RuntimeException("Method " + methodName + " for " +
096                            className + " could not be reconstructed - class reference has gone");
097                }
098                Class<?>[] paramTypes = null;
099                if (writeParamClassNames != null) {
100                    paramTypes = new Class[2];
101                    paramTypes[0] = writeParamTypeRef0.get();
102                    if (paramTypes[0] == null) {
103                        paramTypes[0] = reLoadClass(writeParamClassNames[0]);
104                        if (paramTypes[0] != null) {
105                            // Compiler needs generic.
106                            writeParamTypeRef0 = new WeakReference<>(paramTypes[0]);
107                        }
108                    }
109                    paramTypes[1] = writeParamTypeRef1.get();
110                    if (paramTypes[1] == null) {
111                        paramTypes[1] = reLoadClass(writeParamClassNames[1]);
112                        if (paramTypes[1] != null) {
113                            // Compiler needs generic.
114                            writeParamTypeRef1 = new WeakReference<>(paramTypes[1]);
115                        }
116                    }
117                } else {
118                    paramTypes = STRING_CLASS_PARAMETER;
119                }
120                try {
121                    m = clazz.getMethod(methodName, paramTypes);
122                    // Un-comment following line for testing
123                    // System.out.println("Recreated Method " + methodName + " for " + className);
124                } catch (final NoSuchMethodException e) {
125                    throw new RuntimeException("Method " + methodName + " for " +
126                            className + " could not be reconstructed - method not found");
127                }
128                methodRef = new SoftReference<>(m);
129            }
130            return m;
131        }
132
133        /**
134         * Try to re-load the class
135         */
136        private Class<?> reLoadClass() {
137            return reLoadClass(className);
138        }
139
140        /**
141         * Try to re-load the class
142         */
143        private Class<?> reLoadClass(final String name) {
144
145            ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
146
147            // Try the context class loader
148            if (classLoader != null) {
149                try {
150                    return classLoader.loadClass(name);
151                } catch (final ClassNotFoundException e) {
152                    // ignore
153                }
154            }
155
156            // Try this class's class loader
157            classLoader = MappedPropertyDescriptor.class.getClassLoader();
158            try {
159                return classLoader.loadClass(name);
160            } catch (final ClassNotFoundException e) {
161                return null;
162            }
163        }
164    }
165
166    /**
167     * The parameter types array for the reader method signature.
168     */
169    private static final Class<?>[] STRING_CLASS_PARAMETER = new Class[]{String.class};
170
171    /**
172     * Return a capitalized version of the specified property name.
173     *
174     * @param s The property name
175     */
176    private static String capitalizePropertyName(final String s) {
177        if (s.length() == 0) {
178            return s;
179        }
180
181        final char[] chars = s.toCharArray();
182        chars[0] = Character.toUpperCase(chars[0]);
183        return new String(chars);
184    }
185
186    /**
187     * Find a method on a class with a specified parameter list.
188     */
189    private static Method getMethod(final Class<?> clazz, final String methodName, final Class<?>[] parameterTypes)
190                                           throws IntrospectionException {
191        if (methodName == null) {
192            return null;
193        }
194
195        final Method method = MethodUtils.getMatchingAccessibleMethod(clazz, methodName, parameterTypes);
196        if (method != null) {
197            return method;
198        }
199
200        final int parameterCount = parameterTypes == null ? 0 : parameterTypes.length;
201
202        // No Method found
203        throw new IntrospectionException("No method \"" + methodName +
204                "\" with " + parameterCount + " parameter(s) of matching types.");
205    }
206
207    /**
208     * Find a method on a class with a specified number of parameters.
209     */
210    private static Method getMethod(final Class<?> clazz, final String methodName, final int parameterCount)
211            throws IntrospectionException {
212        if (methodName == null) {
213            return null;
214        }
215
216        final Method method = internalGetMethod(clazz, methodName, parameterCount);
217        if (method != null) {
218            return method;
219        }
220
221        // No Method found
222        throw new IntrospectionException("No method \"" + methodName +
223                "\" with " + parameterCount + " parameter(s)");
224    }
225
226    /**
227     * Find a method on a class with a specified number of parameters.
228     */
229    private static Method internalGetMethod(final Class<?> initial, final String methodName,
230                                            final int parameterCount) {
231        // For overridden methods we need to find the most derived version.
232        // So we start with the given class and walk up the superclass chain.
233        for (Class<?> clazz = initial; clazz != null; clazz = clazz.getSuperclass()) {
234            final Method[] methods = clazz.getDeclaredMethods();
235            for (final Method method : methods) {
236                if (method == null) {
237                    continue;
238                }
239                // skip static methods.
240                final int mods = method.getModifiers();
241                if (!Modifier.isPublic(mods) ||
242                    Modifier.isStatic(mods)) {
243                    continue;
244                }
245                if (method.getName().equals(methodName) &&
246                        method.getParameterTypes().length == parameterCount) {
247                    return method;
248                }
249            }
250        }
251
252        // Now check any inherited interfaces.  This is necessary both when
253        // the argument class is itself an interface, and when the argument
254        // class is an abstract class.
255        final Class<?>[] interfaces = initial.getInterfaces();
256        for (final Class<?> interface1 : interfaces) {
257            final Method method = internalGetMethod(interface1, methodName, parameterCount);
258            if (method != null) {
259                return method;
260            }
261        }
262
263        return null;
264    }
265
266    /**
267     * The underlying data type of the property we are describing.
268     */
269    private Reference<Class<?>> mappedPropertyTypeRef;
270
271    /**
272     * The reader method for this property (if any).
273     */
274    private MappedMethodReference mappedReadMethodRef;
275
276    /**
277     * The writer method for this property (if any).
278     */
279    private MappedMethodReference mappedWriteMethodRef;
280
281    /**
282     * Constructs a MappedPropertyDescriptor for a property that follows
283     * the standard Java convention by having getFoo and setFoo
284     * accessor methods, with the addition of a String parameter (the key).
285     * Thus if the argument name is "fred", it will
286     * assume that the writer method is "setFred" and the reader method
287     * is "getFred".  Note that the property name should start with a lower
288     * case character, which will be capitalized in the method names.
289     *
290     * @param propertyName The programmatic name of the property.
291     * @param beanClass The Class object for the target bean.  For
292     *        example sun.beans.OurButton.class.
293     *
294     * @throws IntrospectionException if an exception occurs during
295     *              introspection.
296     */
297    public MappedPropertyDescriptor(final String propertyName, final Class<?> beanClass)
298            throws IntrospectionException {
299
300        super(propertyName, null, null);
301
302        if (propertyName == null || propertyName.length() == 0) {
303            throw new IntrospectionException("bad property name: " +
304                    propertyName + " on class: " + beanClass.getClass().getName());
305        }
306
307        setName(propertyName);
308        final String base = capitalizePropertyName(propertyName);
309
310        // Look for mapped read method and matching write method
311        Method mappedReadMethod = null;
312        Method mappedWriteMethod = null;
313        try {
314            try {
315                mappedReadMethod = getMethod(beanClass, "get" + base,
316                        STRING_CLASS_PARAMETER);
317            } catch (final IntrospectionException e) {
318                mappedReadMethod = getMethod(beanClass, "is" + base,
319                        STRING_CLASS_PARAMETER);
320            }
321            final Class<?>[] params = { String.class, mappedReadMethod.getReturnType() };
322            mappedWriteMethod = getMethod(beanClass, "set" + base, params);
323        } catch (final IntrospectionException e) {
324            /* Swallow IntrospectionException
325             * TODO: Why?
326             */
327        }
328
329        // If there's no read method, then look for just a write method
330        if (mappedReadMethod == null) {
331            mappedWriteMethod = getMethod(beanClass, "set" + base, 2);
332        }
333
334        if (mappedReadMethod == null && mappedWriteMethod == null) {
335            throw new IntrospectionException("Property '" + propertyName +
336                    "' not found on " +
337                    beanClass.getName());
338        }
339        mappedReadMethodRef  = new MappedMethodReference(mappedReadMethod);
340        mappedWriteMethodRef = new MappedMethodReference(mappedWriteMethod);
341
342        findMappedPropertyType();
343    }
344
345    /**
346     * This constructor takes the name of a mapped property, and method
347     * names for reading and writing the property.
348     *
349     * @param propertyName The programmatic name of the property.
350     * @param beanClass The Class object for the target bean.  For
351     *        example sun.beans.OurButton.class.
352     * @param mappedGetterName The name of the method used for
353     *          reading one of the property values.  May be null if the
354     *          property is write-only.
355     * @param mappedSetterName The name of the method used for writing
356     *          one of the property values.  May be null if the property is
357     *          read-only.
358     *
359     * @throws IntrospectionException if an exception occurs during
360     *              introspection.
361     */
362    public MappedPropertyDescriptor(final String propertyName, final Class<?> beanClass,
363                                    final String mappedGetterName, final String mappedSetterName)
364            throws IntrospectionException {
365
366        super(propertyName, null, null);
367
368        if (propertyName == null || propertyName.length() == 0) {
369            throw new IntrospectionException("bad property name: " +
370                    propertyName);
371        }
372        setName(propertyName);
373
374        // search the mapped get and set methods
375        Method mappedReadMethod = null;
376        Method mappedWriteMethod = null;
377        mappedReadMethod =
378            getMethod(beanClass, mappedGetterName, STRING_CLASS_PARAMETER);
379
380        if (mappedReadMethod != null) {
381            final Class<?>[] params = { String.class, mappedReadMethod.getReturnType() };
382            mappedWriteMethod =
383                getMethod(beanClass, mappedSetterName, params);
384        } else {
385            mappedWriteMethod =
386                getMethod(beanClass, mappedSetterName, 2);
387        }
388        mappedReadMethodRef  = new MappedMethodReference(mappedReadMethod);
389        mappedWriteMethodRef = new MappedMethodReference(mappedWriteMethod);
390
391        findMappedPropertyType();
392    }
393
394    /**
395     * This constructor takes the name of a mapped property, and Method
396     * objects for reading and writing the property.
397     *
398     * @param propertyName The programmatic name of the property.
399     * @param mappedGetter The method used for reading one of
400     *          the property values.  May be be null if the property
401     *          is write-only.
402     * @param mappedSetter The method used for writing one the
403     *          property values.  May be null if the property is read-only.
404     *
405     * @throws IntrospectionException if an exception occurs during
406     *              introspection.
407     */
408    public MappedPropertyDescriptor(final String propertyName,
409                                    final Method mappedGetter, final Method mappedSetter)
410            throws IntrospectionException {
411
412        super(propertyName, mappedGetter, mappedSetter);
413
414        if (propertyName == null || propertyName.length() == 0) {
415            throw new IntrospectionException("bad property name: " +
416                    propertyName);
417        }
418
419        setName(propertyName);
420        mappedReadMethodRef  = new MappedMethodReference(mappedGetter);
421        mappedWriteMethodRef = new MappedMethodReference(mappedSetter);
422        findMappedPropertyType();
423    }
424
425    /**
426     * Introspect our bean class to identify the corresponding getter
427     * and setter methods.
428     */
429    private void findMappedPropertyType() throws IntrospectionException {
430        try {
431            final Method mappedReadMethod  = getMappedReadMethod();
432            final Method mappedWriteMethod = getMappedWriteMethod();
433            Class<?> mappedPropertyType = null;
434            if (mappedReadMethod != null) {
435                if (mappedReadMethod.getParameterTypes().length != 1) {
436                    throw new IntrospectionException
437                            ("bad mapped read method arg count");
438                }
439                mappedPropertyType = mappedReadMethod.getReturnType();
440                if (mappedPropertyType == Void.TYPE) {
441                    throw new IntrospectionException
442                            ("mapped read method " +
443                            mappedReadMethod.getName() + " returns void");
444                }
445            }
446
447            if (mappedWriteMethod != null) {
448                final Class<?>[] params = mappedWriteMethod.getParameterTypes();
449                if (params.length != 2) {
450                    throw new IntrospectionException
451                            ("bad mapped write method arg count");
452                }
453                if (mappedPropertyType != null &&
454                        mappedPropertyType != params[1]) {
455                    throw new IntrospectionException
456                            ("type mismatch between mapped read and write methods");
457                }
458                mappedPropertyType = params[1];
459            }
460            // Compiler needs generic.
461            mappedPropertyTypeRef = new SoftReference<>(mappedPropertyType);
462        } catch (final IntrospectionException ex) {
463            throw ex;
464        }
465    }
466
467    /**
468     * Gets the Class object for the property values.
469     *
470     * @return The Java type info for the property values.  Note that
471     * the "Class" object may describe a built-in Java type such as "int".
472     * The result may be "null" if this is a mapped property that
473     * does not support non-keyed access.
474     * <p>
475     * This is the type that will be returned by the mappedReadMethod.
476     */
477    public Class<?> getMappedPropertyType() {
478        return mappedPropertyTypeRef.get();
479    }
480
481    /**
482     * Gets the method that should be used to read one of the property value.
483     *
484     * @return The method that should be used to read the property value.
485     * May return null if the property can't be read.
486     */
487    public Method getMappedReadMethod() {
488        return mappedReadMethodRef.get();
489    }
490
491    /**
492     * Gets the method that should be used to write one of the property value.
493     *
494     * @return The method that should be used to write one of the property value.
495     * May return null if the property can't be written.
496     */
497    public Method getMappedWriteMethod() {
498        return mappedWriteMethodRef.get();
499    }
500
501    /**
502     * Sets the method that should be used to read one of the property value.
503     *
504     * @param mappedGetter The mapped getter method.
505     * @throws IntrospectionException If an error occurs finding the
506     * mapped property
507     */
508    public void setMappedReadMethod(final Method mappedGetter)
509            throws IntrospectionException {
510        mappedReadMethodRef = new MappedMethodReference(mappedGetter);
511        findMappedPropertyType();
512    }
513
514    /**
515     * Sets the method that should be used to write the property value.
516     *
517     * @param mappedSetter The mapped setter method.
518     * @throws IntrospectionException If an error occurs finding the
519     * mapped property
520     */
521    public void setMappedWriteMethod(final Method mappedSetter)
522            throws IntrospectionException {
523        mappedWriteMethodRef = new MappedMethodReference(mappedSetter);
524        findMappedPropertyType();
525    }
526}