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.InvocationTargetException;
024import java.util.ArrayList;
025import java.util.List;
026
027import org.granite.clustering.TransientReference;
028import org.granite.config.GraniteConfig;
029import org.granite.config.flex.Destination;
030import org.granite.context.GraniteContext;
031import org.granite.logging.Logger;
032import org.granite.messaging.service.security.RemotingDestinationSecurizer;
033
034import flex.messaging.messages.RemotingMessage;
035
036/**
037 * Abstract base class for all service's methods calls. This class mainly implements a final
038 * invocation method which deals with parameter conversions, security and listeners.
039 * 
040 * @author Franck WOLFF
041 *
042 * @see ServiceFactory
043 * @see ServiceInvocationListener
044 * @see ServiceExceptionHandler
045 */
046@TransientReference
047public abstract class ServiceInvoker<T extends ServiceFactory> {
048
049        private static final Logger log = Logger.getLogger(ServiceInvoker.class);
050
051    protected final List<ServiceInvocationListener> invocationListeners;
052    protected final Destination destination;
053    protected final T factory;
054    protected Object invokee = null;
055
056    private ServiceExceptionHandler serviceExceptionHandler;
057
058    /**
059     * Constructs a new ServiceInvoker. This constructor is used by a dedicated {@link ServiceFactory}.
060     * 
061     * @param destination the remote destination of this service (services-config.xml).
062     * @param factory the factory that have called this constructor.
063     * @throws ServiceException if anything goes wrong.
064     */
065    protected ServiceInvoker(Destination destination, T factory) throws ServiceException {
066        this.destination = destination;
067        this.factory = factory;
068        this.serviceExceptionHandler = factory.getServiceExceptionHandler();
069
070        ServiceInvocationListener invocationListener =
071            GraniteContext.getCurrentInstance().getGraniteConfig().getInvocationListener();
072        if (invocationListener != null) {
073            this.invocationListeners = new ArrayList<ServiceInvocationListener>();
074            this.invocationListeners.add(invocationListener);
075        } else
076            this.invocationListeners = null;
077    }
078
079    /**
080     * Called at the beginning of the {@link #invoke(RemotingMessage)} method. Give a chance to modify the
081     * the services (invokee) about to be called. Does nothing by default. The default invokee object is
082     * created by actual implementations of this abstract class.
083     * 
084     * @param request the current remoting message (sent from Flex).
085     * @param methodName the name of the method to be called.
086     * @param args the method parameter values.
087     * @return the (possibly adjusted) invokee object.
088     * @throws ServiceException if anything goes wrong.
089     */
090    protected Object adjustInvokee(RemotingMessage request, String methodName, Object[] args) throws ServiceException {
091        return invokee;
092    }
093
094    /**
095     * Called before the {@link #invoke(RemotingMessage)} method starts to search for a method named <tt>methodName</tt>
096     * with the arguments <tt>args</tt> on the invokee object. Give a chance to modify the method name or the paramaters.
097     * Does nothing by default.
098     * 
099     * @param invokee the service instance used for searching the method with the specified arguments.
100     * @param methodName the method name.
101     * @param args the arguments of the method.
102     * @return an array of containing the (possibly adjusted) method name and its arguments.
103     */
104    protected Object[] beforeMethodSearch(Object invokee, String methodName, Object[] args) {
105        return new Object[] { methodName, args };
106    }
107
108    /**
109     * Called before the invocation of the services method. Does nothing by default.
110     * 
111     * @param context the current invocation context.
112     */
113    protected void beforeInvocation(ServiceInvocationContext context) {
114    }
115
116    /**
117     * Called after a failed invocation of the service's method. Returns <tt>false</tt> by default.
118     * 
119     * @param context the current invocation context.
120     * @param t the exception that caused the invocation failure.
121     * @return <tt>true</tt> if invocation should be retried, <tt>false</tt> otherwise.
122     */
123    protected boolean retryInvocation(ServiceInvocationContext context, Throwable t) {
124        return false;
125    }
126
127    /**
128     * Called after a failed invocation of the service's method, possibly after a new attempt (see
129     * {@link #retryInvocation(ServiceInvocationContext, Throwable)}. Does nothing by default.
130     * 
131     * @param context the current invocation context.
132     * @param error the exception that caused the invocation failure.
133     */
134    protected void afterInvocationError(ServiceInvocationContext context, Throwable error) {
135    }
136
137    /**
138     * Called after a successful invocation of the service's method. Does nothing by default.
139     * 
140     * @param context the current invocation context.
141     * @param result the result of the invocation (returned by the called method).
142     */
143    protected Object afterInvocation(ServiceInvocationContext context, Object result) {
144        return result;
145    }
146
147    /**
148     * Call a service's method according to the informations contained in the given remoting message.
149     * This method is final and implements a standard way of calling a service's method, independent of
150     * the underlying framework (EJB3, Spring, Seam, etc.) It deals with security, parameter conversions,
151     * exception handling and offers several ways of listening (and possibly adjusting) the invocation
152     * process.
153     * 
154     * @param request the remoting message containing informations about the call.
155     * @return the result of the service's method invocation.
156     * @throws ServiceException if anything goes wrong (security, invocation target exception, etc.)
157     */
158    public final Object invoke(RemotingMessage request) throws ServiceException {
159
160        GraniteConfig config = GraniteContext.getCurrentInstance().getGraniteConfig();
161
162        String methodName = request.getOperation();
163        Object[] args = (Object[])request.getBody();
164
165        // Adjust invokee (SimpleServiceInvoker with runtime "source" attribute)
166        Object invokee = adjustInvokee(request, methodName, args);
167
168        // Try to find out method to call (and convert parameters).
169        Object[] call = beforeMethodSearch(invokee, methodName, args);
170        methodName = (String)call[0];
171        args = (Object[])call[1];
172        if (invocationListeners != null) {
173            for (ServiceInvocationListener invocationListener : invocationListeners)
174                args = invocationListener.beforeMethodSearch(invokee, methodName, args);
175        }
176
177        log.debug(">> Trying to find method: %s%s in %s", methodName, args, invokee != null ? invokee.getClass() : "");
178
179        ServiceInvocationContext invocationContext = null;
180        try {
181            invocationContext = config.getMethodMatcher().findServiceMethod(
182                request,
183                destination,
184                invokee,
185                methodName,
186                args
187            );
188        } catch (NoSuchMethodException e) {
189            throw serviceExceptionHandler.handleNoSuchMethodException(
190                request, destination, invokee, methodName, args, e
191            );
192        }
193
194        try {
195                beforeInvocation(invocationContext);
196                if (invocationListeners != null) {
197                    for (ServiceInvocationListener invocationListener : invocationListeners)
198                        invocationListener.beforeInvocation(invocationContext);
199                }
200        }
201        catch (Exception error) {
202                handleInvocationError(invocationContext, error);
203        }
204
205        log.debug(">> Invoking method: %s with ", invocationContext.getMethod(), args);
206
207        Throwable error = null;
208        Object result = null;
209        try {
210
211            // Check security 1 (destination).
212            if (destination.getSecurizer() instanceof RemotingDestinationSecurizer)
213                ((RemotingDestinationSecurizer)destination.getSecurizer()).canExecute(invocationContext);
214
215            boolean retry = false;
216            try {
217                // Check security 2 (security service).
218                    if (config.hasSecurityService() && config.getSecurityService().acceptsContext())
219                        result = config.getSecurityService().authorize(invocationContext);
220                    else
221                        result = invocationContext.invoke();
222            } 
223            catch (Exception e) {
224                if (retryInvocation(invocationContext, (e instanceof InvocationTargetException ? e.getCause() : e)))
225                        retry = true;
226                else
227                        throw e;
228            }
229            
230            if (retry) {
231                // Check security 2 (security service).
232                    if (config.hasSecurityService())
233                        result = config.getSecurityService().authorize(invocationContext);
234                    else
235                        result = invocationContext.invoke();
236            }
237
238        } catch (InvocationTargetException e) {
239                error = e.getTargetException();
240        } catch (Throwable e) {
241                error = e;
242        } finally {
243                if (error != null) {
244                        handleInvocationError(invocationContext, error);
245                }
246        }
247
248        result = afterInvocation(invocationContext, result);
249        if (invocationListeners != null) {
250            for (ServiceInvocationListener invocationListener : invocationListeners)
251                result = invocationListener.afterInvocation(invocationContext, result);
252        }
253
254        log.debug("<< Returning result: %s", result);
255
256        return result;
257    }
258    
259    
260    private void handleInvocationError(ServiceInvocationContext invocationContext, Throwable error) throws ServiceException {
261                afterInvocationError(invocationContext, error);
262        if (invocationListeners != null) {
263            for (ServiceInvocationListener invocationListener : invocationListeners)
264                invocationListener.afterInvocationError(invocationContext, error);
265        }
266        if (error instanceof ServiceException)
267                throw (ServiceException)error;
268                throw serviceExceptionHandler.handleInvocationException(invocationContext, error);
269    }
270}