All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.cometd.server.AbstractService Maven / Gradle / Ivy

There is a newer version: 8.0.6
Show newest version
/*
 * 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.ServerMessage.Mutable;
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;

/**
 * 

{@link AbstractService} provides convenience methods to assist with the * creation of a CometD services.

*

A CometD service runs application code whenever a message is received on * a particular channel.

*

Specifically it provides:

*
    *
  • Mapping of channel subscriptions to method invocation on the derived * service class.
  • *
  • 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).
  • *
  • The objects returned from method invocation are delivered back to the * calling client in a private message.
  • *
*

Subclasses should call {@link #addService(String, String)} in order to * map channel subscriptions to method invocations, usually in the subclass * constructor.

*

Each CometD service has an associated {@link LocalSession} that can be * used as the source for messages published via * {@link ServerChannel#publish(Session, Mutable)} or * {@link ServerSession#deliver(Session, Mutable)}.

* * @see {@link BayeuxServer#newLocalSession(String)} as an alternative to {@link AbstractService}. */ public abstract class AbstractService { protected final Logger _logger = LoggerFactory.getLogger(getClass()); private final Map invokers = new ConcurrentHashMap<>(); private final String _name; private final BayeuxServerImpl _bayeux; private final LocalSession _session; private ThreadPool _threadPool; private boolean _seeOwn = false; /** *

Instantiates a CometD service with the given name.

* * @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); } /** *

Instantiate a CometD service with the given name and max number of pooled threads.

* * @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; } /** *

Sets the thread pool associated to this CometD service.

*

If the {@link ThreadPool} is a {@link LifeCycle} instance, * and it is not already started, then it will started.

* * @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; } /** *

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.

*

The channel name may be a {@link ServerChannel#isWild() wildcard channel name}.

*

The method must have a unique name and the following signature:

*
    *
  • myMethod(ServerSession from, ServerMessage message)
  • *
*

Typically a service will be used to a channel in the /service/** * space which is not a broadcast channel.

*

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.

*

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.

*

A mapped method may also publish to different channels via * {@link ServerChannel#publish(Session, Mutable)}.

* * @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) { _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); } /** *

Unmaps the method with the given name that has been mapped to the given channel.

* * @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); } } /** *

Unmaps all the methods that have been mapped to the given channel.

* * @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); } } } /** *

Sends data to an individual remote client.

*

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.

*

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.

* * @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); } /** *

Handles exceptions during the invocation of a mapped method.

*

This method is called when a mapped method throws and exception while handling a message.

* * @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) { _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, Mutable message) { if (isSeeOwnPublishes() || from != getServerSession()) invoke(method, from, message); return true; } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy