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