001/**
002 *   GRANITE DATA SERVICES
003 *   Copyright (C) 2006-2013 GRANITE DATA SERVICES S.A.S.
004 *
005 *   This file is part of the Granite Data Services Platform.
006 *
007 *   Granite Data Services is free software; you can redistribute it and/or
008 *   modify it under the terms of the GNU Lesser General Public
009 *   License as published by the Free Software Foundation; either
010 *   version 2.1 of the License, or (at your option) any later version.
011 *
012 *   Granite Data Services 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 Lesser
015 *   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 library; if not, write to the Free Software
019 *   Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
020 *   USA, or see <http://www.gnu.org/licenses/>.
021 */
022package org.granite.messaging.service;
023
024import java.lang.reflect.Method;
025import java.lang.reflect.ParameterizedType;
026import java.lang.reflect.Type;
027import java.util.ArrayList;
028import java.util.Arrays;
029import java.util.List;
030
031import org.granite.config.GraniteConfig;
032import org.granite.config.flex.Destination;
033import org.granite.context.GraniteContext;
034import org.granite.logging.Logger;
035import org.granite.messaging.amf.io.convert.Converter;
036import org.granite.messaging.amf.io.convert.Converters;
037import org.granite.messaging.service.annotations.IgnoredMethod;
038import org.granite.messaging.service.annotations.RemoteDestination;
039import org.granite.util.StringUtil;
040import org.granite.util.TypeUtil;
041
042import flex.messaging.messages.Message;
043
044/**
045 * @author Franck WOLFF
046 * @author Pedro GONCALVES
047 */
048public class DefaultMethodMatcher implements MethodMatcher {
049    
050    private static final Logger log = Logger.getLogger(DefaultMethodMatcher.class);
051
052        
053    public ServiceInvocationContext findServiceMethod(
054        Message message,
055        Destination destination,
056        Object service,
057        String methodName,
058        Object[] params) throws NoSuchMethodException {
059
060        GraniteConfig config = GraniteContext.getCurrentInstance().getGraniteConfig();
061        Converters converters = config.getConverters();
062
063        Class<?> serviceClass = service.getClass();
064        ParameterizedType[] serviceDeclaringTypes = TypeUtil.getDeclaringTypes(serviceClass);
065
066        MatchingMethod match = null;
067        if (params == null || params.length == 0)
068            match = new MatchingMethod(serviceClass.getMethod(methodName, (Class[])null), null);
069        else {
070            List<MatchingMethod> matchingMethods = new ArrayList<MatchingMethod>();
071            
072            List<Method> methods = new ArrayList<Method>();
073            for (Class<?> serviceInterface : serviceClass.getInterfaces())
074                methods.addAll(Arrays.asList(serviceInterface.getMethods()));
075            
076            methods.addAll(Arrays.asList(serviceClass.getMethods()));
077            
078            for (Method method : methods) {
079
080                if (!methodName.equals(method.getName()))
081                    continue;
082
083                Type[] paramTypes = method.getGenericParameterTypes();
084                if (paramTypes.length != params.length)
085                    continue;
086                
087                // Methods marked with @IgnoredMethod cannot be called remotely
088                if (method.isAnnotationPresent(IgnoredMethod.class))
089                        continue;
090                
091                findAndChange(paramTypes, method.getDeclaringClass(), serviceDeclaringTypes);
092
093                Converter[] convertersArray = getConvertersArray(converters, params, paramTypes);
094                if (convertersArray != null)
095                    matchingMethods.add(new MatchingMethod(method, convertersArray));
096            }
097            
098            if (matchingMethods.size() == 1)
099                match = matchingMethods.get(0);
100            else if (matchingMethods.size() > 1) {
101                // Multiple matches found
102                match = resolveMatchingMethod(matchingMethods, serviceClass);
103            }
104        }
105
106        if (match == null)
107            throw new NoSuchMethodException(serviceClass.getName() + '.' + methodName + StringUtil.toString(params));
108
109        params = convert(match.convertersArray, params, match.serviceMethod.getGenericParameterTypes());
110
111        return new ServiceInvocationContext(message, destination, service, match.serviceMethod, params);
112    }
113
114    protected Converter[] getConvertersArray(Converters converters, Object[] values, Type[] targetTypes) {
115        Converter[] convertersArray = new Converter[values.length];
116        for (int i = 0; i < values.length; i++) {
117            convertersArray[i] = converters.getConverter(values[i], targetTypes[i]);
118            if (convertersArray[i] == null)
119                return null;
120        }
121        return convertersArray;
122    }
123
124    protected Object[] convert(Converter[] convertersArray, Object[] values, Type[] targetTypes) {
125        if (values.length > 0) {
126            for (int i = 0; i < convertersArray.length; i++)
127                values[i] = convertersArray[i].convert(values[i], targetTypes[i]);
128        }
129        return values;
130    }
131    
132    protected MatchingMethod resolveMatchingMethod(List<MatchingMethod> methods, Class<?> serviceClass) {
133
134        // Prefer methods of interfaces/classes marked with @RemoteDestination
135        for (MatchingMethod m : methods) {
136            if (m.serviceMethod.getDeclaringClass().isAnnotationPresent(RemoteDestination.class))
137                return m;
138        }
139        
140        // Then prefer method declared by the serviceClass (with EJBs, we have always 2 methods, one in the interface,
141        // the other in the proxy, and the @RemoteDestination annotation cannot be find on the proxy class even
142        // it is present on the original class).
143        List<MatchingMethod> serviceClassMethods = new ArrayList<MatchingMethod>();
144        for (MatchingMethod m : methods) {
145            if (m.serviceMethod.getDeclaringClass().equals(serviceClass))
146                serviceClassMethods.add(m);
147        }
148        if (serviceClassMethods.size() == 1)
149                return serviceClassMethods.get(0);
150        
151        log.warn("Ambiguous method match for " + methods.get(0).serviceMethod.getName() + ", selecting first found " + methods.get(0).serviceMethod);        
152        return methods.get(0);
153    }
154    
155    /**
156     * If there is only one TypeVariable in method's argument list, it will be replaced
157     * by the type of the superclass of the service.
158     */
159    protected void findAndChange(Type[] paramTypes, Class<?> declaringClass, ParameterizedType[] declaringTypes) {
160        for (int j = 0; j < paramTypes.length; j++)
161            paramTypes[j] = TypeUtil.resolveTypeVariable(paramTypes[j], declaringClass, declaringTypes);
162    }
163   
164    /**
165     * Returns actual type argument of a given class.
166     */
167    protected Type getGenericType(Class<?> clazz) {
168        try {
169            ParameterizedType genericSuperclass = (ParameterizedType)clazz.getGenericSuperclass();
170            Type[] actualTypeArguments = genericSuperclass.getActualTypeArguments();
171            if (actualTypeArguments != null && actualTypeArguments.length == 1)
172                return actualTypeArguments[0];
173        } catch (Exception e) {
174                // fallback...
175        }
176        return null;
177    }
178    
179    private static class MatchingMethod {
180        
181        public final Method serviceMethod;
182        public final Converter[] convertersArray;
183
184        public MatchingMethod(Method serviceMethod, Converter[] convertersArray) {
185                        this.serviceMethod = serviceMethod;
186                        this.convertersArray = convertersArray;
187                }
188
189                @Override
190                public String toString() {
191                        return "MatchingMethod {serviceMethod=" + serviceMethod + ", convertersArray=" + (convertersArray != null ? Arrays.toString(convertersArray) : "[]") + "}";
192                }
193    }
194}