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