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