    001    /*****************************************************************************
    002     * Copyright (C) PicoContainer Organization. All rights reserved.            *
    003     * ------------------------------------------------------------------------- *
    004     * The software in this package is published under the terms of the BSD      *
    005     * style license a copy of which has been included with this distribution in *
    006     * the LICENSE.txt file.                                                     *
    007     *                                                                           *
    008     * Original code by Centerline Computers, Inc.                               *
    009     *****************************************************************************/
    010    package org.picocontainer.gems.util;
    011
    012    import java.lang.reflect.InvocationTargetException;
    013    import java.lang.reflect.Method;
    014    import java.lang.reflect.Modifier;
    015    import java.util.Arrays;
    016
    017    /**
    018     * The DelegateMethod class has been designed in the hope of providing easier
    019     * access to methods invoked via reflection. Sample:
    020     *
    021     * <pre>
    022     * //Sample Map
    023     * HashMap&lt;String, String&gt; testMap = new HashMap&lt;String, String&gt;();
    024     * testMap.put(&quot;a&quot;, &quot;A&quot;);
    025     *
    026     * //Create delegate method that calls the 'clear' method for HashMap.
    027     * DelegateMethod&lt;Map, Void&gt; method = new DelegateMethod&lt;Map, Void&gt;(Map.class,
    028     *              &quot;clear&quot;);
    029     *
    030     * //Invokes clear() on the HashMap.
    031     * method.invoke(testMap);
    032     * </pre>
    033     *
    034     * <p>
    035     * Good uses of this object are for lazy invocation of a method and integrating
    036     * reflection with a vistor pattern.
    037     * </p>
    038     *
    039     * @author Michael Rimov
    040     */
    041    public class DelegateMethod<TARGET_TYPE, RETURN_TYPE> {
    042
    043            /**
    044             * Arguments for the method invocation.
    045             */
    046            private final Object[] args;
    047
    048            /**
    049             * The method to be invoked.
    050             */
    051            private final Method method;
    052
    053            /**
    054             * Constructs a delegate method object that will invoke method
    055             * <em>methodName</em> on class <em>type</em> with the parameters
    056             * specified. The object automatically searches for a suitable object to be
    057             * invoked.
    058             * <p>
    059             * Note that this version simply grabs the
    060             * <em>first<em> method that fits the parameter criteria with
    061             * the specific name.  You may need to be careful if use extensive overloading.</p>
    062             * <p>To specify the exact types in the method.
    063             * @param type the class of the object that should be invoked.
    064             * @param methodName the name of the method that will be invoked.
    065             * @param parameters the parameters to be used.
    066             * @throws NoSuchMethodRuntimeException if the method is not found or parameters that match cannot be found.
    067             */
    068            public DelegateMethod(final Class<TARGET_TYPE> type,
    069                            final String methodName, final Object... parameters)
    070                            throws NoSuchMethodRuntimeException {
    071                    this.args = parameters;
    072                    this.method = findMatchingMethod(type.getMethods(), methodName,
    073                                    parameters);
    074
    075                    if (method == null) {
    076                            throw new NoSuchMethodRuntimeException("Could not find method "
    077                                            + methodName + " in type " + type.getName());
    078                    }
    079            }
    080
    081            /**
    082             * Constructs a DelegateMethod object with very specific argument types.
    083             *
    084             * @param type
    085             *            the type of the class to be examined for reflection.
    086             * @param methodName
    087             *            the name of the method to be invoked.
    088             * @param paramTypes
    089             *            specific parameter types for the method to be found.
    090             * @param parameters
    091             *            the parameters for method invocation.
    092             * @throws NoSuchMethodRuntimeException
    093             *             if the method is not found.
    094             */
    095            public DelegateMethod(final Class<?> type, final String methodName,
    096                            final Class<?>[] paramTypes, final Object... parameters)
    097                            throws NoSuchMethodRuntimeException {
    098                    this.args = parameters;
    099                    try {
    100                            this.method = type.getMethod(methodName, paramTypes);
    101                    } catch (NoSuchMethodException e) {
    102                            throw new NoSuchMethodRuntimeException("Could not find method "
    103                                            + methodName + " in type " + type.getName());
    104                    }
    105            }
    106
    107            /**
    108             * Constructs a method delegate with an explicit Method object.
    109             *
    110             * @param targetMethod
    111             * @param parameters
    112             */
    113            public DelegateMethod(final Method targetMethod, final Object... parameters) {
    114                    this.args = parameters;
    115                    this.method = targetMethod;
    116            }
    117
    118            /**
    119             * Locates a method that fits the given parameter types.
    120             *
    121             * @param methods
    122             * @param methodName
    123             * @param parameters
    124             * @return
    125             */
    126            private Method findMatchingMethod(final Method[] methods,
    127                            final String methodName, final Object[] parameters) {
    128
    129                    // Get parameter types.
    130                    Class<?>[] paramTypes = new Class[parameters.length];
    131                    for (int i = 0; i < parameters.length; i++) {
    132                            if (parameters[i] == null) {
    133                                    paramTypes[i] = NullType.class;
    134                            } else {
    135                                    paramTypes[i] = parameters[i].getClass();
    136                            }
    137                    }
    138
    139                    for (Method eachMethod : methods) {
    140                            if (eachMethod.getName().equals(methodName)) {
    141                                    if (isPotentialMatchingArguments(eachMethod, paramTypes)) {
    142                                            return eachMethod;
    143                                    }
    144                            }
    145                    }
    146
    147                    return null;
    148            }
    149
    150            /**
    151             * Returns true if all parameter types are assignable to the argument type.
    152             *
    153             * @param eachMethod
    154             *            the method we're checking.
    155             * @param paramTypes
    156             *            the parameter types provided as constructor arguments.
    157             * @return true if the given method is a match given the parameter types.
    158             */
    159            private boolean isPotentialMatchingArguments(final Method eachMethod,
    160                            final Class<?>[] paramTypes) {
    161                    Class<?>[] argParameters = eachMethod.getParameterTypes();
    162                    if (argParameters.length != paramTypes.length) {
    163                            return false;
    164                    }
    165
    166                    for (int i = 0; i < paramTypes.length; i++) {
    167                            if (paramTypes[i].getName().equals(NullType.class.getName())) {
    168                                    // Nulls are allowed for any parameter.
    169                                    continue;
    170                            }
    171
    172                            if (!argParameters[i].isAssignableFrom(paramTypes[i])) {
    173                                    return false;
    174                            }
    175                    }
    176
    177                    return true;
    178            }
    179
    180            /**
    181             * Used for invoking static methods on the type passed into the constructor.
    182             *
    183             * @return the result of the invocation. May be null if the return type is
    184             *         void.
    185             * @throws IllegalArgumentException
    186             *             if the method being invoked is not static.
    187             * @throws IllegalAccessRuntimeException
    188             *             if the method being invoked is not public.
    189             * @throws InvocationTargetRuntimeException
    190             *             if an exception is thrown within the method being invoked.
    191             */
    192            public RETURN_TYPE invoke() throws IllegalArgumentException,
    193                            IllegalAccessRuntimeException, InvocationTargetRuntimeException {
    194                    if (!Modifier.isStatic(method.getModifiers())) {
    195                            throw new IllegalArgumentException("Method "
    196                                            + method.toGenericString()
    197                                            + " is not static.  Use invoke(Object) instead.");
    198                    }
    199
    200                    return invoke(null);
    201            }
    202
    203            @SuppressWarnings("unchecked")
    204            private RETURN_TYPE cast(final Object objectToCast) {
    205                    return (RETURN_TYPE) objectToCast;
    206            }
    207
    208            /**
    209             * Invokes the method specified in the constructor against the target
    210             * specified.
    211             *
    212             * @param <V>
    213             *            a subclass of the type specified by the object declaration.
    214             *            This allows Map delegates to operate on HashMaps etc.
    215             * @param target
    216             *            the target object instance to be operated upon. Unless
    217             *            invoking a static method, this should not be null.
    218             * @return the result of the invocation. May be null if the return type is
    219             *         void.
    220             * @throws IllegalArgumentException
    221             *             if the method being invoked is not static and parameter
    222             *             target null.
    223             * @throws IllegalAccessRuntimeException
    224             *             if the method being invoked is not public.
    225             * @throws InvocationTargetRuntimeException
    226             *             if an exception is thrown within the method being invoked.
    227             */
    228            public <V extends TARGET_TYPE> RETURN_TYPE invoke(final V target)
    229                            throws IllegalAccessRuntimeException,
    230                            InvocationTargetRuntimeException {
    231                    assert args != null;
    232
    233                    if (!Modifier.isStatic(method.getModifiers()) && target == null) {
    234                            throw new IllegalArgumentException("Method "
    235                                            + method.toGenericString()
    236                                            + " is not static.  Use invoke(Object) instead.");
    237                    }
    238
    239                    RETURN_TYPE result;
    240                    try {
    241                            result = cast(method.invoke(target, args));
    242                    } catch (IllegalAccessException e) {
    243                            throw new IllegalAccessRuntimeException("Method "
    244                                            + method.toGenericString() + " is not public.", e);
    245                    } catch (InvocationTargetException e) {
    246                            // Unwrap the exception. Should save confusing duplicate traces.
    247                            throw new InvocationTargetRuntimeException(
    248                                            "There was an error invoking " + method.toGenericString(),
    249                                            e.getCause());
    250                    }
    251
    252                    return result;
    253            }
    254
    255            /** {@inheritDoc} */
    256            @Override
    257            public String toString() {
    258                    return "DelegateMethod " + method.toGenericString()
    259                                    + " with arguments: " + Arrays.deepToString(args);
    260            }
    261
    262            /** {@inheritDoc} */
    263            @Override
    264            public int hashCode() {
    265                    final int prime = 31;
    266                    int result = 1;
    267                    result = prime * result + Arrays.hashCode(args);
    268                    result = prime * result + ((method == null) ? 0 : method.hashCode());
    269                    return result;
    270            }
    271
    272            /** {@inheritDoc} */
    273            @Override
    274            @SuppressWarnings("unchecked")
    275            public boolean equals(final Object obj) {
    276                    if (this == obj) {
    277                            return true;
    278                    }
    279                    if (obj == null) {
    280                            return false;
    281                    }
    282                    if (getClass() != obj.getClass()) {
    283                            return false;
    284                    }
    285                    final DelegateMethod other = (DelegateMethod) obj;
    286                    if (!Arrays.equals(args, other.args)) {
    287                            return false;
    288                    }
    289                    if (method == null) {
    290                            if (other.method != null) {
    291                                    return false;
    292                            }
    293                    } else if (!method.equals(other.method)) {
    294                            return false;
    295                    }
    296                    return true;
    297            }
    298
    299            /**
    300             * Retrieves the expected return type of the delegate method.
    301             * @return
    302             */
    303            public Class<?> getReturnType() {
    304                    return method.getReturnType();
    305            }
    306
    307            /**
    308             * Placeholder type used for comparing null parameter values.
    309             *
    310             * @author Michael Rimov
    311             */
    312            private static final class NullType {
    313
    314                    /**
    315                     * This type should never be constructed.
    316                     */
    317                    private NullType() {
    318
    319                    }
    320            }
    321
    322    }