Package org.cometd.server

Source Code of org.cometd.server.AbstractService$Invoker

/*
* Copyright (c) 2008-2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*     http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.cometd.server;

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import org.cometd.bayeux.Session;
import org.cometd.bayeux.server.BayeuxServer;
import org.cometd.bayeux.server.LocalSession;
import org.cometd.bayeux.server.ServerChannel;
import org.cometd.bayeux.server.ServerMessage;
import org.cometd.bayeux.server.ServerSession;
import org.eclipse.jetty.util.component.LifeCycle;
import org.eclipse.jetty.util.thread.QueuedThreadPool;
import org.eclipse.jetty.util.thread.ThreadPool;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* <p>{@link AbstractService} provides convenience methods to assist with the
* creation of a CometD services.</p>
* <p>A CometD service runs application code whenever a message is received on
* a particular channel.</p>
* <p>Specifically it provides:</p>
* <ul>
* <li>Mapping of channel subscriptions to method invocation on the derived
* service class.</li>
* <li>Optional use of a thread pool used for method invocation if handling
* can take considerable time and it is desired not to hold up the delivering
* thread (typically a HTTP request handling thread).</li>
* <li>The objects returned from method invocation are delivered back to the
* calling client in a private message.</li>
* </ul>
* <p>Subclasses should call {@link #addService(String, String)} in order to
* map channel subscriptions to method invocations, usually in the subclass
* constructor.</p>
* <p>Each CometD service has an associated {@link LocalSession} that can be
* used as the source for messages published via
* {@link ServerChannel#publish(Session, ServerMessage.Mutable)} or
* {@link ServerSession#deliver(Session, ServerMessage.Mutable)}.</p>
*
* @see BayeuxServer#newLocalSession(String)
*/
public abstract class AbstractService
{
    protected final Logger _logger = LoggerFactory.getLogger(getClass());
    private final Map<String, Invoker> invokers = new ConcurrentHashMap<>();
    private final String _name;
    private final BayeuxServerImpl _bayeux;
    private final LocalSession _session;
    private ThreadPool _threadPool;
    private boolean _seeOwn = false;

    /**
     * <p>Instantiates a CometD service with the given name.</p>
     *
     * @param bayeux The BayeuxServer instance.
     * @param name   The name of the service (used as client ID prefix).
     */
    public AbstractService(BayeuxServer bayeux, String name)
    {
        this(bayeux, name, 0);
    }

    /**
     * <p>Instantiate a CometD service with the given name and max number of pooled threads.</p>
     *
     * @param bayeux     The BayeuxServer instance.
     * @param name       The name of the service (used as client ID prefix).
     * @param maxThreads The max size of a ThreadPool to create to handle messages.
     */
    public AbstractService(BayeuxServer bayeux, String name, int maxThreads)
    {
        _name = name;
        _bayeux = (BayeuxServerImpl)bayeux;
        _session = _bayeux.newLocalSession(name);
        _session.handshake();
        if (maxThreads > 0)
            setThreadPool(new QueuedThreadPool(maxThreads));
        if (!Modifier.isPublic(getClass().getModifiers()))
            throw new IllegalArgumentException("Service class '" + getClass().getName() + "' must be public");
    }

    public BayeuxServer getBayeux()
    {
        return _bayeux;
    }

    public String getName()
    {
        return _name;
    }

    /**
     * @return The {@link LocalSession} associated with this CometD service
     */
    public LocalSession getLocalSession()
    {
        return _session;
    }

    /**
     * @return The {@link ServerSession} of the {@link LocalSession} associated
     *         with this CometD service
     */
    public ServerSession getServerSession()
    {
        return _session.getServerSession();
    }

    /**
     * @return The thread pool associated with this CometD service, or null
     * @see #AbstractService(BayeuxServer, String, int)
     */
    public ThreadPool getThreadPool()
    {
        return _threadPool;
    }

    /**
     * <p>Sets the thread pool associated to this CometD service.</p>
     * <p>If the {@link ThreadPool} is a {@link LifeCycle} instance,
     * and it is not already started, then it will started.</p>
     *
     * @param pool The ThreadPool
     */
    public void setThreadPool(ThreadPool pool)
    {
        try
        {
            if (pool instanceof LifeCycle)
                if (!((LifeCycle)pool).isStarted())
                    ((LifeCycle)pool).start();
        }
        catch (Exception e)
        {
            throw new IllegalStateException(e);
        }
        _threadPool = pool;
    }

    /**
     * @return whether this CometD service receives messages published by itself
     *         on channels it is subscribed to (defaults to false).
     * @see #setSeeOwnPublishes(boolean)
     */
    public boolean isSeeOwnPublishes()
    {
        return _seeOwn;
    }

    /**
     * @param seeOwnPublishes whether this CometD service receives messages published by itself
     *                        on channels it is subscribed to (defaults to false).
     * @see #isSeeOwnPublishes()
     */
    public void setSeeOwnPublishes(boolean seeOwnPublishes)
    {
        _seeOwn = seeOwnPublishes;
    }

    /**
     * <p>Maps the method of a subclass with the given name to a
     * {@link ServerChannel.MessageListener} on the given channel, so that the method
     * is invoked for each message received on the channel.</p>
     * <p>The channel name may be a {@link ServerChannel#isWild() wildcard channel name}.</p>
     * <p>The method must have a unique name and the following signature:</p>
     * <ul>
     * <li><code>myMethod(ServerSession from, ServerMessage message)</code></li>
     * </ul>
     * <p>Typically a service will be used to a channel in the <code>/service/**</code>
     * space which is not a broadcast channel.</p>
     * <p>Any object returned by a mapped method is delivered back to the
     * client that sent the message and not broadcast. If the method returns void or null,
     * then no response is sent.</p>
     * <p>A mapped method may also call {@link #send(org.cometd.bayeux.server.ServerSession, String, Object)}
     * to deliver message(s) to specific clients and/or channels.</p>
     * <p>A mapped method may also publish to different channels via
     * {@link ServerChannel#publish(Session, ServerMessage.Mutable)}.</p>
     *
     * @param channelName The channel to listen to
     * @param methodName  The name of the method on this subclass to call when messages
     *                    are received on the channel
     * @see #removeService(String, String)
     */
    protected void addService(String channelName, String methodName)
    {
        if (_logger.isDebugEnabled())
            _logger.debug("Mapping {}#{} to {}", _name, methodName, channelName);

        Method candidate = null;
        Class<?> c = this.getClass();
        while (c != null && c != AbstractService.class)
        {
            Method[] methods = c.getDeclaredMethods();
            for (int i = methods.length; i-- > 0; )
            {
                Method method = methods[i];
                if (methodName.equals(method.getName()) && Modifier.isPublic(method.getModifiers()))
                {
                    if (candidate != null)
                        throw new IllegalArgumentException("Multiple service methods called '" + methodName + "'");
                    candidate = method;
                }
            }
            c = c.getSuperclass();
        }

        if (candidate == null)
            throw new NoSuchMethodError("Cannot find public service method '" + methodName + "'");
        int params = candidate.getParameterTypes().length;
        if (params != 2)
            throw new IllegalArgumentException("Service method '" + methodName + "' must have 2 parameters");
        if (!ServerSession.class.isAssignableFrom(candidate.getParameterTypes()[0]))
            throw new IllegalArgumentException("Service method '" + methodName + "' does not have " + ServerSession.class.getName() + " as first parameter");
        if (!ServerMessage.class.isAssignableFrom(candidate.getParameterTypes()[1]))
            throw new IllegalArgumentException("Service method '" + methodName + "' does not have " + ServerMessage.class.getName() + " as second parameter");

        ServerChannel channel = _bayeux.createChannelIfAbsent(channelName).getReference();
        Invoker invoker = new Invoker(channelName, candidate);
        channel.addListener(invoker);
        invokers.put(methodName, invoker);
    }

    /**
     * <p>Unmaps the method with the given name that has been mapped to the given channel.</p>
     *
     * @param channelName The channel name
     * @param methodName  The name of the method to unmap
     * @see #addService(String, String)
     * @see #removeService(String)
     */
    protected void removeService(String channelName, String methodName)
    {
        ServerChannel channel = _bayeux.getChannel(channelName);
        if (channel != null)
        {
            Invoker invoker = invokers.remove(methodName);
            channel.removeListener(invoker);
        }
    }

    /**
     * <p>Unmaps all the methods that have been mapped to the given channel.</p>
     *
     * @param channelName The channel name
     * @see #addService(String, String)
     * @see #removeService(String, String)
     */
    protected void removeService(String channelName)
    {
        ServerChannel channel = _bayeux.getChannel(channelName);
        if (channel != null)
        {
            for (Invoker invoker : invokers.values())
            {
                if (invoker.channelName.equals(channelName))
                    channel.removeListener(invoker);
            }
        }
    }

    /**
     * <p>Sends data to an individual remote client.</p>
     * <p>The data passed is sent to the client
     * as the "data" member of a message with the given channel. The
     * message is not published on the channel and is thus not broadcast to all
     * channel subscribers, but instead delivered directly to the target client.</p>
     * <p>Typically this method is only required if a service method sends
     * response(s) to clients other than the sender, or on different channels.
     * If the response is to be sent to the sender on the same channel,
     * then the data can simply be the return value of the method.</p>
     *
     * @param toClient  The target client
     * @param onChannel The channel of the message
     * @param data      The data of the message
     */
    protected void send(ServerSession toClient, String onChannel, Object data)
    {
        toClient.deliver(_session.getServerSession(), onChannel, data);
    }

    /**
     * <p>Handles exceptions during the invocation of a mapped method.</p>
     * <p>This method is called when a mapped method throws and exception while handling a message.</p>
     *
     * @param method     the name of the method invoked that threw an exception
     * @param session the remote session that sent the message
     * @param local   the local session associated to this service
     * @param message        the message sent by the remote session
     * @param x          the exception thrown
     */
    protected void exception(String method, ServerSession session, LocalSession local, ServerMessage message, Throwable x)
    {
        _logger.info("Exception while invoking " + _name + "#" + method + " from " + session + " with " + message, x);
    }

    private void invoke(final Method method, final ServerSession fromClient, final ServerMessage msg)
    {
        if (_logger.isDebugEnabled())
            _logger.debug("Invoking {}#{} from {} with {}", _name, method.getName(), fromClient, msg);

        ThreadPool threadPool = getThreadPool();
        if (threadPool == null)
        {
            doInvoke(method, fromClient, msg);
        }
        else
        {
            threadPool.execute(new Runnable()
            {
                public void run()
                {
                    doInvoke(method, fromClient, msg);
                }
            });
        }
    }

    protected void doInvoke(Method method, ServerSession session, ServerMessage message)
    {
        try
        {
            Object reply = method.invoke(this, session, message);
            if (reply != null)
                send(session, message.getChannel(), reply);
        }
        catch (Throwable x)
        {
            exception(method.toString(), session, _session, message, x);
        }
    }

    private class Invoker implements ServerChannel.MessageListener
    {
        private final String channelName;
        private final Method method;

        public Invoker(String channelName, Method method)
        {
            this.channelName = channelName;
            this.method = method;
        }

        public boolean onMessage(ServerSession from, ServerChannel channel, ServerMessage.Mutable message)
        {
            if (isSeeOwnPublishes() || from != getServerSession())
                invoke(method, from, message);
            return true;
        }
    }
}
TOP

Related Classes of org.cometd.server.AbstractService$Invoker

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.