001    /*
002     * JBoss DNA (http://www.jboss.org/dna)
003     * See the COPYRIGHT.txt file distributed with this work for information
004     * regarding copyright ownership.  Some portions may be licensed
005     * to Red Hat, Inc. under one or more contributor license agreements.
006     * See the AUTHORS.txt file in the distribution for a full listing of 
007     * individual contributors. 
008     *
009     * JBoss DNA is free software. Unless otherwise indicated, all code in JBoss DNA
010     * is licensed to you under the terms of the GNU Lesser General Public License as
011     * published by the Free Software Foundation; either version 2.1 of
012     * the License, or (at your option) any later version.
013     *
014     * JBoss DNA is distributed in the hope that it will be useful,
015     * but WITHOUT ANY WARRANTY; without even the implied warranty of
016     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
017     * Lesser General Public License for more details.
018     *
019     * You should have received a copy of the GNU Lesser General Public
020     * License along with this software; if not, write to the Free
021     * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
022     * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
023     */
024    package org.jboss.dna.common.util;
025    
026    import java.lang.reflect.InvocationTargetException;
027    import java.lang.reflect.Method;
028    import java.util.ArrayList;
029    import java.util.Collections;
030    import java.util.HashMap;
031    import java.util.HashSet;
032    import java.util.Iterator;
033    import java.util.LinkedList;
034    import java.util.List;
035    import java.util.Map;
036    import java.util.Set;
037    import java.util.regex.Pattern;
038    
039    /**
040     * Utility class for working reflectively with objects.
041     * 
042     * @author Randall Hauch
043     */
044    public class Reflection {
045    
046        /**
047         * Build the list of classes that correspond to the list of argument objects.
048         * 
049         * @param arguments the list of argument objects.
050         * @return the list of Class instances that correspond to the list of argument objects; the resulting list will contain a null
051         *         element for each null argument.
052         */
053        public static Class<?>[] buildArgumentClasses( Object... arguments ) {
054            if (arguments == null || arguments.length == 0) return EMPTY_CLASS_ARRAY;
055            Class<?>[] result = new Class<?>[arguments.length];
056            int i = 0;
057            for (Object argument : arguments) {
058                if (argument != null) {
059                    result[i] = argument.getClass();
060                } else {
061                    result[i] = null;
062                }
063            }
064            return result;
065        }
066    
067        /**
068         * Build the list of classes that correspond to the list of argument objects.
069         * 
070         * @param arguments the list of argument objects.
071         * @return the list of Class instances that correspond to the list of argument objects; the resulting list will contain a null
072         *         element for each null argument.
073         */
074        public static List<Class<?>> buildArgumentClassList( Object... arguments ) {
075            if (arguments == null || arguments.length == 0) return Collections.emptyList();
076            List<Class<?>> result = new ArrayList<Class<?>>(arguments.length);
077            for (Object argument : arguments) {
078                if (argument != null) {
079                    result.add(argument.getClass());
080                } else {
081                    result.add(null);
082                }
083            }
084            return result;
085        }
086    
087        /**
088         * Convert any argument classes to primitives.
089         * 
090         * @param arguments the list of argument classes.
091         * @return the list of Class instances in which any classes that could be represented by primitives (e.g., Boolean) were
092         *         replaced with the primitive classes (e.g., Boolean.TYPE).
093         */
094        public static List<Class<?>> convertArgumentClassesToPrimitives( Class<?>... arguments ) {
095            if (arguments == null || arguments.length == 0) return Collections.emptyList();
096            List<Class<?>> result = new ArrayList<Class<?>>(arguments.length);
097            for (Class<?> clazz : arguments) {
098                if (clazz == Boolean.class) clazz = Boolean.TYPE;
099                else if (clazz == Character.class) clazz = Character.TYPE;
100                else if (clazz == Byte.class) clazz = Byte.TYPE;
101                else if (clazz == Short.class) clazz = Short.TYPE;
102                else if (clazz == Integer.class) clazz = Integer.TYPE;
103                else if (clazz == Long.class) clazz = Long.TYPE;
104                else if (clazz == Float.class) clazz = Float.TYPE;
105                else if (clazz == Double.class) clazz = Double.TYPE;
106                else if (clazz == Void.class) clazz = Void.TYPE;
107                result.add(clazz);
108            }
109    
110            return result;
111        }
112    
113        /**
114         * Returns the name of the class. The result will be the fully-qualified class name, or the readable form for arrays and
115         * primitive types.
116         * 
117         * @param clazz the class for which the class name is to be returned.
118         * @return the readable name of the class.
119         */
120        public static String getClassName( final Class<?> clazz ) {
121            final String fullName = clazz.getName();
122            final int fullNameLength = fullName.length();
123    
124            // Check for array ('[') or the class/interface marker ('L') ...
125            int numArrayDimensions = 0;
126            while (numArrayDimensions < fullNameLength) {
127                final char c = fullName.charAt(numArrayDimensions);
128                if (c != '[') {
129                    String name = null;
130                    // Not an array, so it must be one of the other markers ...
131                    switch (c) {
132                        case 'L': {
133                            name = fullName.subSequence(numArrayDimensions + 1, fullNameLength).toString();
134                            break;
135                        }
136                        case 'B': {
137                            name = "byte";
138                            break;
139                        }
140                        case 'C': {
141                            name = "char";
142                            break;
143                        }
144                        case 'D': {
145                            name = "double";
146                            break;
147                        }
148                        case 'F': {
149                            name = "float";
150                            break;
151                        }
152                        case 'I': {
153                            name = "int";
154                            break;
155                        }
156                        case 'J': {
157                            name = "long";
158                            break;
159                        }
160                        case 'S': {
161                            name = "short";
162                            break;
163                        }
164                        case 'Z': {
165                            name = "boolean";
166                            break;
167                        }
168                        case 'V': {
169                            name = "void";
170                            break;
171                        }
172                        default: {
173                            name = fullName.subSequence(numArrayDimensions, fullNameLength).toString();
174                        }
175                    }
176                    if (numArrayDimensions == 0) {
177                        // No array markers, so just return the name ...
178                        return name;
179                    }
180                    // Otherwise, add the array markers and the name ...
181                    if (numArrayDimensions < BRACKETS_PAIR.length) {
182                        name = name + BRACKETS_PAIR[numArrayDimensions];
183                    } else {
184                        for (int i = 0; i < numArrayDimensions; i++) {
185                            name = name + BRACKETS_PAIR[1];
186                        }
187                    }
188                    return name;
189                }
190                ++numArrayDimensions;
191            }
192    
193            return fullName;
194        }
195    
196        private static final Class<?>[] EMPTY_CLASS_ARRAY = new Class[] {};
197        private static final String[] BRACKETS_PAIR = new String[] {"", "[]", "[][]", "[][][]", "[][][][]", "[][][][][]"};
198    
199        private Class<?> targetClass;
200        private Map<String, LinkedList<Method>> methodMap = null; // used for the brute-force method finder
201    
202        /**
203         * Construct a Reflection instance that cache's some information about the target class. The target class is the Class object
204         * upon which the methods will be found.
205         * 
206         * @param targetClass the target class
207         * @throws IllegalArgumentException if the target class is null
208         */
209        public Reflection( Class<?> targetClass ) {
210            CheckArg.isNotNull(targetClass, "targetClass");
211            this.targetClass = targetClass;
212        }
213    
214        /**
215         * Return the class that is the target for the reflection methods.
216         * 
217         * @return the target class
218         */
219        public Class<?> getTargetClass() {
220            return this.targetClass;
221        }
222    
223        /**
224         * Find the method on the target class that matches the supplied method name.
225         * 
226         * @param methodName the name of the method that is to be found.
227         * @param caseSensitive true if the method name supplied should match case-sensitively, or false if case does not matter
228         * @return the Method objects that have a matching name, or an empty array if there are no methods that have a matching name.
229         */
230        public Method[] findMethods( String methodName,
231                                     boolean caseSensitive ) {
232            Pattern pattern = caseSensitive ? Pattern.compile(methodName) : Pattern.compile(methodName, Pattern.CASE_INSENSITIVE);
233            return findMethods(pattern);
234        }
235    
236        /**
237         * Find the methods on the target class that matches the supplied method name.
238         * 
239         * @param methodNamePattern the regular expression pattern for the name of the method that is to be found.
240         * @return the Method objects that have a matching name, or an empty array if there are no methods that have a matching name.
241         */
242        public Method[] findMethods( Pattern methodNamePattern ) {
243            final Method[] allMethods = this.targetClass.getMethods();
244            final List<Method> result = new ArrayList<Method>();
245            for (int i = 0; i < allMethods.length; i++) {
246                final Method m = allMethods[i];
247                if (methodNamePattern.matcher(m.getName()).matches()) {
248                    result.add(m);
249                }
250            }
251            return result.toArray(new Method[result.size()]);
252        }
253    
254        /**
255         * Find the getter methods on the target class that begin with "get" or "is", that have no parameters, and that return
256         * something other than void. This method skips the {@link Object#getClass()} method.
257         * 
258         * @return the Method objects for the getters; never null but possibly empty
259         */
260        public Method[] findGetterMethods() {
261            final Method[] allMethods = this.targetClass.getMethods();
262            final List<Method> result = new ArrayList<Method>();
263            for (int i = 0; i < allMethods.length; i++) {
264                final Method m = allMethods[i];
265                int numParams = m.getParameterTypes().length;
266                if (numParams != 0) continue;
267                String name = m.getName();
268                if (name.equals("getClass")) continue;
269                if (m.getReturnType() == Void.TYPE) continue;
270                if (name.startsWith("get") || name.startsWith("is")) {
271                    result.add(m);
272                }
273            }
274            return result.toArray(new Method[result.size()]);
275        }
276    
277        /**
278         * Find the property names with getter methods on the target class. This method returns the property names for the methods
279         * returned by {@link #findGetterMethods()}.
280         * 
281         * @return the Java Bean property names for the getters; never null but possibly empty
282         */
283        public String[] findGetterPropertyNames() {
284            final Method[] getters = findGetterMethods();
285            final List<String> result = new ArrayList<String>();
286            for (int i = 0; i < getters.length; i++) {
287                final Method m = getters[i];
288                String name = m.getName();
289                if (name.startsWith("get") && name.length() > 3) {
290                    result.add(name.substring(3));
291                } else if (name.startsWith("is") && name.length() > 2) {
292                    result.add(name.substring(2));
293                }
294            }
295            return result.toArray(new String[result.size()]);
296        }
297    
298        /**
299         * Find the method on the target class that matches the supplied method name.
300         * 
301         * @param methodName the name of the method that is to be found.
302         * @param caseSensitive true if the method name supplied should match case-sensitively, or false if case does not matter
303         * @return the first Method object found that has a matching name, or null if there are no methods that have a matching name.
304         */
305        public Method findFirstMethod( String methodName,
306                                       boolean caseSensitive ) {
307            Pattern pattern = caseSensitive ? Pattern.compile(methodName) : Pattern.compile(methodName, Pattern.CASE_INSENSITIVE);
308            return findFirstMethod(pattern);
309        }
310    
311        /**
312         * Find the method on the target class that matches the supplied method name.
313         * 
314         * @param methodNamePattern the regular expression pattern for the name of the method that is to be found.
315         * @return the first Method object found that has a matching name, or null if there are no methods that have a matching name.
316         */
317        public Method findFirstMethod( Pattern methodNamePattern ) {
318            final Method[] allMethods = this.targetClass.getMethods();
319            for (int i = 0; i < allMethods.length; i++) {
320                final Method m = allMethods[i];
321                if (methodNamePattern.matcher(m.getName()).matches()) {
322                    return m;
323                }
324            }
325            return null;
326        }
327    
328        /**
329         * Find and execute the best method on the target class that matches the signature specified with one of the specified names
330         * and the list of arguments. If no such method is found, a NoSuchMethodException is thrown.
331         * <P>
332         * This method is unable to find methods with signatures that include both primitive arguments <i>and</i> arguments that are
333         * instances of <code>Number</code> or its subclasses.
334         * </p>
335         * 
336         * @param methodNames the names of the methods that are to be invoked, in the order they are to be tried
337         * @param target the object on which the method is to be invoked
338         * @param arguments the array of Object instances that correspond to the arguments passed to the method.
339         * @return the Method object that references the method that satisfies the requirements, or null if no satisfactory method
340         *         could be found.
341         * @throws NoSuchMethodException if a matching method is not found.
342         * @throws SecurityException if access to the information is denied.
343         * @throws InvocationTargetException
344         * @throws IllegalAccessException
345         * @throws IllegalArgumentException
346         */
347        public Object invokeBestMethodOnTarget( String[] methodNames,
348                                                Object target,
349                                                Object... arguments )
350            throws NoSuchMethodException, SecurityException, IllegalArgumentException, IllegalAccessException,
351            InvocationTargetException {
352            Class<?>[] argumentClasses = buildArgumentClasses(arguments);
353            int remaining = methodNames.length;
354            Object result = null;
355            for (String methodName : methodNames) {
356                --remaining;
357                try {
358                    Method method = findBestMethodWithSignature(methodName, argumentClasses);
359                    result = method.invoke(target, arguments);
360                    break;
361                } catch (NoSuchMethodException e) {
362                    if (remaining == 0) throw e;
363                }
364            }
365            return result;
366        }
367    
368        /**
369         * Find and execute the best setter method on the target class for the supplied property name and the supplied list of
370         * arguments. If no such method is found, a NoSuchMethodException is thrown.
371         * <P>
372         * This method is unable to find methods with signatures that include both primitive arguments <i>and</i> arguments that are
373         * instances of <code>Number</code> or its subclasses.
374         * </p>
375         * 
376         * @param javaPropertyName the name of the property whose setter is to be invoked, in the order they are to be tried
377         * @param target the object on which the method is to be invoked
378         * @param argument the new value for the property
379         * @return the result of the setter method, which is typically null (void)
380         * @throws NoSuchMethodException if a matching method is not found.
381         * @throws SecurityException if access to the information is denied.
382         * @throws InvocationTargetException
383         * @throws IllegalAccessException
384         * @throws IllegalArgumentException
385         */
386        public Object invokeSetterMethodOnTarget( String javaPropertyName,
387                                                  Object target,
388                                                  Object argument )
389            throws NoSuchMethodException, SecurityException, IllegalArgumentException, IllegalAccessException,
390            InvocationTargetException {
391            String[] methodNamesArray = findMethodNames("set" + javaPropertyName);
392            return invokeBestMethodOnTarget(methodNamesArray, target, argument);
393        }
394    
395        /**
396         * Find and execute the getter method on the target class for the supplied property name. If no such method is found, a
397         * NoSuchMethodException is thrown.
398         * 
399         * @param javaPropertyName the name of the property whose getter is to be invoked, in the order they are to be tried
400         * @param target the object on which the method is to be invoked
401         * @return the property value (the result of the getter method call)
402         * @throws NoSuchMethodException if a matching method is not found.
403         * @throws SecurityException if access to the information is denied.
404         * @throws InvocationTargetException
405         * @throws IllegalAccessException
406         * @throws IllegalArgumentException
407         */
408        public Object invokeGetterMethodOnTarget( String javaPropertyName,
409                                                  Object target )
410            throws NoSuchMethodException, SecurityException, IllegalArgumentException, IllegalAccessException,
411            InvocationTargetException {
412            String[] methodNamesArray = findMethodNames("get" + javaPropertyName);
413            return invokeBestMethodOnTarget(methodNamesArray, target);
414        }
415    
416        protected String[] findMethodNames( String methodName ) {
417            Method[] methods = findMethods(methodName, false);
418            Set<String> methodNames = new HashSet<String>();
419            for (Method method : methods) {
420                String actualMethodName = method.getName();
421                methodNames.add(actualMethodName);
422            }
423            return methodNames.toArray(new String[methodNames.size()]);
424        }
425    
426        /**
427         * Find the best method on the target class that matches the signature specified with the specified name and the list of
428         * arguments. This method first attempts to find the method with the specified arguments; if no such method is found, a
429         * NoSuchMethodException is thrown.
430         * <P>
431         * This method is unable to find methods with signatures that include both primitive arguments <i>and</i> arguments that are
432         * instances of <code>Number</code> or its subclasses.
433         * 
434         * @param methodName the name of the method that is to be invoked.
435         * @param arguments the array of Object instances that correspond to the arguments passed to the method.
436         * @return the Method object that references the method that satisfies the requirements, or null if no satisfactory method
437         *         could be found.
438         * @throws NoSuchMethodException if a matching method is not found.
439         * @throws SecurityException if access to the information is denied.
440         */
441        public Method findBestMethodOnTarget( String methodName,
442                                              Object... arguments ) throws NoSuchMethodException, SecurityException {
443            Class<?>[] argumentClasses = buildArgumentClasses(arguments);
444            return findBestMethodWithSignature(methodName, argumentClasses);
445        }
446    
447        /**
448         * Find the best method on the target class that matches the signature specified with the specified name and the list of
449         * argument classes. This method first attempts to find the method with the specified argument classes; if no such method is
450         * found, a NoSuchMethodException is thrown.
451         * 
452         * @param methodName the name of the method that is to be invoked.
453         * @param argumentsClasses the list of Class instances that correspond to the classes for each argument passed to the method.
454         * @return the Method object that references the method that satisfies the requirements, or null if no satisfactory method
455         *         could be found.
456         * @throws NoSuchMethodException if a matching method is not found.
457         * @throws SecurityException if access to the information is denied.
458         */
459        public Method findBestMethodWithSignature( String methodName,
460                                                   Class<?>... argumentsClasses ) throws NoSuchMethodException, SecurityException {
461    
462            // Attempt to find the method
463            Method result;
464    
465            // -------------------------------------------------------------------------------
466            // First try to find the method with EXACTLY the argument classes as specified ...
467            // -------------------------------------------------------------------------------
468            Class<?>[] classArgs = null;
469            try {
470                classArgs = argumentsClasses != null ? argumentsClasses : new Class[] {};
471                result = this.targetClass.getMethod(methodName, classArgs); // this may throw an exception if not found
472                return result;
473            } catch (NoSuchMethodException e) {
474                // No method found, so continue ...
475            }
476    
477            // ---------------------------------------------------------------------------------------------
478            // Then try to find a method with the argument classes converted to a primitive, if possible ...
479            // ---------------------------------------------------------------------------------------------
480            List<Class<?>> argumentsClassList = convertArgumentClassesToPrimitives(argumentsClasses);
481            try {
482                classArgs = argumentsClassList.toArray(new Class[argumentsClassList.size()]);
483                result = this.targetClass.getMethod(methodName, classArgs); // this may throw an exception if not found
484                return result;
485            } catch (NoSuchMethodException e) {
486                // No method found, so continue ...
487            }
488    
489            // ---------------------------------------------------------------------------------------------
490            // Still haven't found anything. So far, the "getMethod" logic only finds methods that EXACTLY
491            // match the argument classes (i.e., not methods declared with superclasses or interfaces of
492            // the arguments). There is no canned algorithm in Java to do this, so we have to brute-force it.
493            // The following algorithm will find the first method that matches by doing "instanceof", so it
494            // may not be the best method. Since there is some overhead to this algorithm, the first time
495            // caches some information in class members.
496            // ---------------------------------------------------------------------------------------------
497            Method method;
498            LinkedList<Method> methodsWithSameName;
499            if (this.methodMap == null) {
500                this.methodMap = new HashMap<String, LinkedList<Method>>();
501                Method[] methods = this.targetClass.getMethods();
502                for (int i = 0; i != methods.length; ++i) {
503                    method = methods[i];
504                    methodsWithSameName = this.methodMap.get(method.getName());
505                    if (methodsWithSameName == null) {
506                        methodsWithSameName = new LinkedList<Method>();
507                        this.methodMap.put(method.getName(), methodsWithSameName);
508                    }
509                    methodsWithSameName.addFirst(method); // add lower methods first
510                }
511            }
512    
513            // ------------------------------------------------------------------------
514            // Find the set of methods with the same name (do this twice, once with the
515            // original methods and once with the primitives) ...
516            // ------------------------------------------------------------------------
517            // List argClass = argumentsClasses;
518            for (int j = 0; j != 2; ++j) {
519                methodsWithSameName = this.methodMap.get(methodName);
520                if (methodsWithSameName == null) {
521                    throw new NoSuchMethodException(methodName);
522                }
523                Iterator<Method> iter = methodsWithSameName.iterator();
524                Class<?>[] args;
525                Class<?> clazz;
526                boolean allMatch;
527                while (iter.hasNext()) {
528                    method = iter.next();
529                    args = method.getParameterTypes();
530                    if (args.length == argumentsClassList.size()) {
531                        allMatch = true; // assume they all match
532                        for (int i = 0; i < args.length; ++i) {
533                            clazz = argumentsClassList.get(i);
534                            if (clazz != null) {
535                                if (!args[i].isAssignableFrom(clazz)) {
536                                    allMatch = false; // found one that doesn't match
537                                    i = args.length; // force completion
538                                }
539                            } else {
540                                // a null is assignable for everything except a primitive
541                                if (args[i].isPrimitive()) {
542                                    allMatch = false; // found one that doesn't match
543                                    i = args.length; // force completion
544                                }
545                            }
546                        }
547                        if (allMatch) {
548                            return method;
549                        }
550                    }
551                }
552            }
553    
554            throw new NoSuchMethodException(methodName);
555        }
556    
557    }