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