package com.kixeye.kixmpp.server; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.SettableFuture; import com.google.common.util.concurrent.Striped; import com.kixeye.kixmpp.*; import com.kixeye.kixmpp.handler.KixmppEventEngine; import com.kixeye.kixmpp.interceptor.KixmppStanzaInterceptor; import com.kixeye.kixmpp.p2p.ClusterClient; import com.kixeye.kixmpp.p2p.discovery.ConstNodeDiscovery; import com.kixeye.kixmpp.p2p.discovery.NodeDiscovery; import com.kixeye.kixmpp.p2p.listener.ClusterListener; import com.kixeye.kixmpp.p2p.node.NodeId; import com.kixeye.kixmpp.server.cluster.mapreduce.MapReduceTracker; import com.kixeye.kixmpp.server.cluster.message.*; import com.kixeye.kixmpp.server.module.KixmppServerModule; import com.kixeye.kixmpp.server.module.auth.SaslKixmppServerModule; import com.kixeye.kixmpp.server.module.bind.BindKixmppServerModule; import com.kixeye.kixmpp.server.module.chat.ChatKixmppServerModule; import com.kixeye.kixmpp.server.module.disco.DiscoKixmppServerModule; import com.kixeye.kixmpp.server.module.features.FeaturesKixmppServerModule; import com.kixeye.kixmpp.server.module.muc.*; import com.kixeye.kixmpp.server.module.presence.PresenceKixmppServerModule; import com.kixeye.kixmpp.server.module.roster.RosterKixmppServerModule; import com.kixeye.kixmpp.server.module.session.SessionKixmppServerModule; import io.netty.bootstrap.ServerBootstrap; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.*; import io.netty.channel.epoll.EpollEventLoopGroup; import io.netty.channel.epoll.EpollServerSocketChannel; import io.netty.channel.group.DefaultChannelGroup; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioServerSocketChannel; import io.netty.handler.codec.http.*; import io.netty.handler.codec.http.websocketx.*; import io.netty.util.CharsetUtil; import io.netty.util.concurrent.Future; import io.netty.util.concurrent.GenericFutureListener; import io.netty.util.concurrent.GlobalEventExecutor; import io.netty.util.concurrent.Promise; import org.fusesource.hawtdispatch.Task; import org.jdom2.Element; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.net.InetSocketAddress; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.Map.Entry; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.locks.Lock; /* * #%L * KIXMPP * %% * Copyright (C) 2014 KIXEYE, Inc * %% * 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. * #L% */ /** * A XMPP server. * * @author ebahtijaragic */ public class KixmppServer implements AutoCloseable, ClusterListener { private static final Logger logger = LoggerFactory.getLogger(KixmppServer.class); private static String OS = System.getProperty("os.name").toLowerCase(); public static final InetSocketAddress DEFAULT_SOCKET_ADDRESS = new InetSocketAddress(5222); public static final InetSocketAddress DEFAULT_WEBSOCKET_ADDRESS = new InetSocketAddress(5290); public static final InetSocketAddress DEFAULT_CLUSTER_ADDRESS = new InetSocketAddress(8100); public static final int CUSTOM_MESSAGE_START = 16; private final InetSocketAddress bindAddress; private final String domain; private final ServerBootstrap bootstrap; private InetSocketAddress webSocketAddress; private ServerBootstrap webSocketBootstrap; private final KixmppEventEngine eventEngine; private final Set<String> modulesToRegister = Collections.newSetFromMap(new ConcurrentHashMap<String, Boolean>()); private final ConcurrentHashMap<String, KixmppServerModule> modules = new ConcurrentHashMap<>(); private final Set<KixmppStanzaInterceptor> interceptors = Collections.newSetFromMap(new ConcurrentHashMap<KixmppStanzaInterceptor, Boolean>()); private final AtomicReference<ChannelFuture> channelFuture = new AtomicReference<>(); private final AtomicReference<Channel> channel = new AtomicReference<>(); private final AtomicReference<ChannelFuture> webSocketChannelFuture = new AtomicReference<>(); private final AtomicReference<Channel> webSocketChannel = new AtomicReference<>(); private AtomicReference<State> state = new AtomicReference<>(State.STOPPED); private final DefaultChannelGroup channels; private final ConcurrentHashMap<KixmppJid, Channel> jidChannel = new ConcurrentHashMap<>(); private final ConcurrentHashMap<String, Set<Channel>> usernameChannel = new ConcurrentHashMap<>(); private final Striped<Lock> usernameChannelStripes = Striped.lock(Runtime.getRuntime().availableProcessors() * 4); private MucRoomEventHandler mucRoomEventHandler = new DefaultMucRoomEventHandler(); private static enum State { STARTING, STARTED, STOPPING, STOPPED } private final ClusterClient cluster; private final MapReduceTracker mapReduce; /** * Creates a new {@link KixmppServer} with the given ssl engine. * * @param domain */ public KixmppServer(String domain) { this(DEFAULT_SOCKET_ADDRESS, domain, DEFAULT_CLUSTER_ADDRESS, new ConstNodeDiscovery() ); } public MucRoomEventHandler getMucRoomEventHandler() { return mucRoomEventHandler; } public void setMucRoomEventHandler(MucRoomEventHandler mucRoomEventHandler) { this.mucRoomEventHandler = mucRoomEventHandler; } /** * Creates a new {@link KixmppServer} with the given ssl engine. * * @param bindAddress * @param domain */ public KixmppServer(InetSocketAddress bindAddress, String domain) { this(bindAddress, domain, DEFAULT_CLUSTER_ADDRESS, new ConstNodeDiscovery()); } /** * Creates a new {@link KixmppServer} with the given ssl engine. * * @param bindAddress * @param domain */ public KixmppServer(InetSocketAddress bindAddress, String domain, InetSocketAddress clusterAddress, NodeDiscovery clusterDiscovery) { this(bindAddress, domain, clusterAddress, clusterDiscovery, true); } /** * Creates a new {@link KixmppServer} with the given ssl engine. * * @param bindAddress * @param domain */ public KixmppServer(InetSocketAddress bindAddress, String domain, InetSocketAddress clusterAddress, NodeDiscovery clusterDiscovery, boolean useEpollIfAvailable) { if (useEpollIfAvailable && OS.indexOf("nux") >= 0) { this.bootstrap = new ServerBootstrap() .group(new EpollEventLoopGroup(), new EpollEventLoopGroup()) .channel(EpollServerSocketChannel.class) .childHandler(new ChannelInitializer<SocketChannel>() { protected void initChannel(SocketChannel ch) throws Exception { ch.pipeline().addLast(new KixmppCodec()); ch.pipeline().addLast(new KixmppServerMessageHandler()); } }); } else { this.bootstrap = new ServerBootstrap() .group(new NioEventLoopGroup(), new NioEventLoopGroup()) .channel(NioServerSocketChannel.class) .childHandler(new ChannelInitializer<SocketChannel>() { protected void initChannel(SocketChannel ch) throws Exception { ch.pipeline().addLast(new KixmppCodec()); ch.pipeline().addLast(new KixmppServerMessageHandler()); } }); } this.cluster = new ClusterClient( this, clusterAddress.getHostName(), clusterAddress.getPort(), clusterDiscovery, 300000, bootstrap.group() ); this.cluster.getMessageRegistry().addCustomMessage(1, RoomBroadcastTask.class); this.cluster.getMessageRegistry().addCustomMessage(2, RoomPresenceBroadcastTask.class); this.cluster.getMessageRegistry().addCustomMessage(3, PrivateChatTask.class); this.cluster.getMessageRegistry().addCustomMessage(4, GetMucRoomNicknamesRequest.class); this.cluster.getMessageRegistry().addCustomMessage(5, GetMucRoomNicknamesResponse.class); this.mapReduce = new MapReduceTracker(this, bootstrap.group()); this.channels = new DefaultChannelGroup("All Channels", GlobalEventExecutor.INSTANCE); this.bindAddress = bindAddress; this.domain = domain.toLowerCase(); this.eventEngine = new KixmppEventEngine(); this.modulesToRegister.add(FeaturesKixmppServerModule.class.getName()); this.modulesToRegister.add(SaslKixmppServerModule.class.getName()); this.modulesToRegister.add(BindKixmppServerModule.class.getName()); this.modulesToRegister.add(SessionKixmppServerModule.class.getName()); this.modulesToRegister.add(PresenceKixmppServerModule.class.getName()); this.modulesToRegister.add(MucKixmppServerModule.class.getName()); this.modulesToRegister.add(RosterKixmppServerModule.class.getName()); this.modulesToRegister.add(DiscoKixmppServerModule.class.getName()); this.modulesToRegister.add(ChatKixmppServerModule.class.getName()); } /** * Enables the WebSocket port. */ public KixmppServer enableWebSocket() { return enableWebSocket(DEFAULT_WEBSOCKET_ADDRESS); } /** * Enables the WebSocket port. * * @param webSocketAddress */ public KixmppServer enableWebSocket(InetSocketAddress webSocketAddress) { if (state.get() != State.STOPPED) { throw new IllegalStateException(String.format("The current state is [%s] but must be [STOPPED]", state.get())); } this.webSocketAddress = webSocketAddress; if (this.bootstrap.group() instanceof EpollEventLoopGroup && this.bootstrap.childGroup() instanceof EpollEventLoopGroup) { this.webSocketBootstrap = new ServerBootstrap() .group(this.bootstrap.group(), this.bootstrap.childGroup()) .channel(EpollServerSocketChannel.class) .childHandler(new ChannelInitializer<SocketChannel>() { protected void initChannel(SocketChannel ch) throws Exception { ch.pipeline().addLast(new HttpServerCodec()); ch.pipeline().addLast(new HttpObjectAggregator(65536)); ch.pipeline().addLast(new WebSocketServerHandler()); ch.pipeline().addLast(new KixmppWebSocketCodec()); ch.pipeline().addLast(new KixmppServerMessageHandler()); } }); } else { this.webSocketBootstrap = new ServerBootstrap() .group(this.bootstrap.group(), this.bootstrap.childGroup()) .channel(NioServerSocketChannel.class) .childHandler(new ChannelInitializer<SocketChannel>() { protected void initChannel(SocketChannel ch) throws Exception { ch.pipeline().addLast(new HttpServerCodec()); ch.pipeline().addLast(new HttpObjectAggregator(65536)); ch.pipeline().addLast(new WebSocketServerHandler()); ch.pipeline().addLast(new KixmppWebSocketCodec()); ch.pipeline().addLast(new KixmppServerMessageHandler()); } }); } return this; } /** * Starts the server. * * @throws Exception */ public ListenableFuture<KixmppServer> start() throws Exception { checkAndSetState(State.STARTING, State.STOPPED); logger.info("Starting Kixmpp Server on [{}]...", bindAddress); // register all modules for (String moduleClassName : modulesToRegister) { installModule(moduleClassName); } final SettableFuture<KixmppServer> responseFuture = SettableFuture.create(); final GenericFutureListener<Future<? super Void>> channelFutureListener = new GenericFutureListener<Future<? super Void>>() { @Override public synchronized void operationComplete(Future<? super Void> future) throws Exception { if (webSocketChannelFuture.get() != null && webSocketChannelFuture.get().isDone()) { if (webSocketChannelFuture.get().isSuccess()) { logger.info("Kixmpp WebSocket Server listening on [{}]", webSocketAddress); webSocketChannel.set(webSocketChannelFuture.get().channel()); if (channelFuture.get() == null && !responseFuture.isDone()) { logger.info("Started Kixmpp Server"); state.set(State.STARTED); responseFuture.set(KixmppServer.this); } webSocketChannelFuture.set(null); } else { logger.error("Unable to start Kixmpp WebSocket Server on [{}]", webSocketAddress, future.cause()); if (channelFuture.get() == null && !responseFuture.isDone()) { state.set(State.STOPPED); responseFuture.setException(future.cause()); } webSocketChannelFuture.set(null); } } else if (channelFuture.get() != null && channelFuture.get().isDone()) { if (channelFuture.get().isSuccess()) { logger.info("Kixmpp Server listening on [{}]", bindAddress); channel.set(channelFuture.get().channel()); if (webSocketChannelFuture.get() == null && !responseFuture.isDone()) { logger.info("Started Kixmpp Server"); state.set(State.STARTED); responseFuture.set(KixmppServer.this); } channelFuture.set(null); } else { logger.error("Unable to start Kixmpp Server on [{}]", bindAddress, future.cause()); if (webSocketChannelFuture.get() == null && !responseFuture.isDone()) { state.set(State.STOPPED); responseFuture.setException(future.cause()); } channelFuture.set(null); } } } }; channelFuture.set(bootstrap.bind(bindAddress)); channelFuture.get().addListener(channelFutureListener); if (webSocketAddress != null && webSocketBootstrap != null) { webSocketChannelFuture.set(webSocketBootstrap.bind(webSocketAddress)); webSocketChannelFuture.get().addListener(channelFutureListener); } return responseFuture; } /** * Stops the server. * * @return */ public ListenableFuture<KixmppServer> stop() { checkAndSetState(State.STOPPING, State.STARTED, State.STARTING); logger.info("Stopping Kixmpp Server..."); // shutdown clustering cluster.shutdown(); for (Entry<String, KixmppServerModule> entry : modules.entrySet()) { entry.getValue().uninstall(this); } final SettableFuture<KixmppServer> responseFuture = SettableFuture.create(); ChannelFuture serverChannelFuture = channelFuture.get(); if (serverChannelFuture != null) { serverChannelFuture.cancel(true); } ChannelFuture webSocketServerChannelFuture = webSocketChannelFuture.get(); if (webSocketServerChannelFuture != null) { webSocketServerChannelFuture.cancel(true); } final Channel serverChannel = channel.get(); final Channel webSocketServerChannel = webSocketChannel.get(); if (serverChannel == null && webSocketServerChannel == null) { logger.info("Stopped Kixmpp Server"); state.set(State.STOPPED); responseFuture.set(KixmppServer.this); } else { final GenericFutureListener<Future<? super Void>> channelFutureListener = new GenericFutureListener<Future<? super Void>>() { public synchronized void operationComplete(Future<? super Void> future) throws Exception { if ((serverChannel != null && !serverChannel.isActive()) && (webSocketServerChannel != null && !webSocketServerChannel.isActive())) { logger.info("Stopped Kixmpp Server"); state.set(State.STOPPED); eventEngine.unregisterAll(); responseFuture.set(KixmppServer.this); } } }; if (serverChannel != null) { serverChannel.disconnect().addListener(channelFutureListener); } if (webSocketServerChannel != null) { webSocketServerChannel.disconnect().addListener(channelFutureListener); } } return responseFuture; } /** * @see java.lang.AutoCloseable#close() */ public void close() throws Exception { stop(); } /** * Sets Netty {@link ChannelOption}s. * * @param option * @param value * @return */ public <T> KixmppServer channelOption(ChannelOption<T> option, T value) { bootstrap.option(option, value); return this; } /** * Sets Netty child {@link ChannelOption}s. * * @param option * @param value * @return */ public <T> KixmppServer childChannelOption(ChannelOption<T> option, T value) { bootstrap.childOption(option, value); return this; } /** * @param moduleClass * @return true if module is installed */ public boolean hasActiveModule(Class<?> moduleClass) { return modules.containsKey(moduleClass.getName()); } /** * Gets or installs a module. * * @param moduleClass * @return */ @SuppressWarnings("unchecked") public <T extends KixmppServerModule> T module(Class<T> moduleClass) { T module = (T)modules.get(moduleClass.getName()); if (module == null) { module = (T)installModule(moduleClass.getName()); } return module; } /** * Returns a collections of active modules. * * @return */ public Collection<KixmppServerModule> modules() { return modules.values(); } /** * Gets the event engine. * * @return */ public KixmppEventEngine getEventEngine() { return eventEngine; } /** * @return the bindAddress */ public InetSocketAddress getBindAddress() { return bindAddress; } /** * @return the bindAddress */ public InetSocketAddress getWebSocketAddress() { return webSocketAddress; } /** * @return the domain */ public String getDomain() { return domain; } /** * Adds a stanza interceptor. * * @param interceptor */ public boolean addInterceptor(KixmppStanzaInterceptor interceptor) { return interceptors.add(interceptor); } /** * Removes a stanza interceptor. * * @param interceptor */ public boolean removeInterceptor(KixmppStanzaInterceptor interceptor) { return interceptors.remove(interceptor); } /** * Gets the number of channels. * * @return */ public int getChannelCount() { return channels.size(); } /** * Gets a channel that is assigned to this JID. * * @param jid * @return */ public Channel getChannel(KixmppJid jid) { return jidChannel.get(jid); } /** * Gets channel by username. * * @param username * @return */ @SuppressWarnings("unchecked") public Set<Channel> getChannels(String username) { Set<Channel> channels = usernameChannel.get(username); if (channels != null) { return Collections.unmodifiableSet(channels); } else { return Collections.EMPTY_SET; } } /** * Adds a channel mapping. * * @param jid * @param channel */ public void addChannelMapping(KixmppJid jid, Channel channel) { jidChannel.put(jid, channel); Lock lock = usernameChannelStripes.get(jid.getNode()); try { lock.lock(); Set<Channel> channels = usernameChannel.get(jid.getNode()); if (channels == null) { channels = new HashSet<>(); } channels.add(channel); usernameChannel.put(jid.getNode(), channels); } finally { lock.unlock(); } } /** * Tries to install module. * * @param moduleClassName */ private KixmppServerModule installModule(String moduleClassName) { KixmppServerModule module = null; try { module = (KixmppServerModule)Class.forName(moduleClassName).newInstance(); module.install(this); modules.put(moduleClassName, module); } catch (Exception e) { logger.error("Error while installing module", e); } return module; } /** * Checks the state and sets it. * * @param update * @param expectedStates * @throws IllegalStateException */ private void checkAndSetState(State update, State... expectedStates) throws IllegalStateException { if (expectedStates != null) { boolean wasSet = false; for (State expectedState : expectedStates) { if (state.compareAndSet(expectedState, update)) { wasSet = true; break; } } if (!wasSet) { throw new IllegalStateException(String.format("The current state is [%s] but must be [%s]", state.get(), expectedStates)); } } else { if (!state.compareAndSet(null, update)) { throw new IllegalStateException(String.format("The current state is [%s] but must be [null]", state.get())); } } } /** * Message handler for the {@link KixmppServer} * * @author ebahtijaragic */ private final class KixmppServerMessageHandler extends ChannelDuplexHandler { @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { if (msg instanceof Element) { Element stanza = (Element)msg; boolean rejected = false; for (KixmppStanzaInterceptor interceptor : interceptors) { try { interceptor.interceptIncoming(ctx.channel(),(Element)msg); } catch (KixmppStanzaRejectedException e) { rejected = true; logger.debug("Incoming stanza interceptor [{}] threw an rejected exception.", interceptor, e); } catch (Exception e) { logger.error("Incoming stanza interceptor [{}] threw an exception.", interceptor, e); } } if (!rejected) { eventEngine.publishStanza(ctx.channel(), stanza); } } else if (msg instanceof KixmppStreamStart) { KixmppStreamStart streamStart = (KixmppStreamStart)msg; eventEngine.publishStreamStart(ctx.channel(), streamStart); } else if (msg instanceof KixmppStreamEnd) { KixmppStreamEnd streamEnd = (KixmppStreamEnd)msg; eventEngine.publishStreamEnd(ctx.channel(), streamEnd); } else { logger.error("Unknown message type [{}] from Channel [{}]", msg, ctx.channel()); } } @Override public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { boolean rejected = false; if (msg instanceof Element) { for (KixmppStanzaInterceptor interceptor : interceptors) { try { interceptor.interceptOutgoing(ctx.channel(), (Element)msg); } catch (KixmppStanzaRejectedException e) { rejected = true; logger.debug("Outgoing stanza interceptor [{}] threw an rejected exception.", interceptor, e); } catch (Exception e) { logger.error("Outgoing stanza interceptor [{}] threw an exception.", interceptor, e); } } } if (!rejected) { super.write(ctx, msg, promise); } } @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { logger.debug("Channel [{}] connected.", ctx.channel()); channels.add(ctx.channel()); eventEngine.publishConnected(ctx.channel()); } @Override public void channelInactive(ChannelHandlerContext ctx) throws Exception { logger.debug("Channel [{}] disconnected.", ctx.channel()); channels.remove(ctx.channel()); KixmppJid jid = ctx.channel().attr(BindKixmppServerModule.JID).get(); if (jid != null) { jidChannel.remove(jid); Lock lock = usernameChannelStripes.get(jid.getNode()); try { lock.lock(); usernameChannel.remove(jid.getNode()); } finally { lock.unlock(); } } eventEngine.publishDisconnected(ctx.channel()); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { logger.error("Unexpected exception.", cause); ctx.close(); } } public class WebSocketServerHandler extends SimpleChannelInboundHandler<Object> { private WebSocketServerHandshaker handshaker; @Override public void channelRead0(ChannelHandlerContext ctx, Object msg) { if (msg instanceof FullHttpRequest) { handleHttpRequest(ctx, (FullHttpRequest) msg); } else if (msg instanceof WebSocketFrame) { handleWebSocketFrame(ctx, (WebSocketFrame) msg); } } @Override public void channelReadComplete(ChannelHandlerContext ctx) { ctx.flush(); } private void handleHttpRequest(ChannelHandlerContext ctx, FullHttpRequest req) { // Handle a bad request. if (!req.getDecoderResult().isSuccess()) { sendHttpResponse(ctx, req, new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.BAD_REQUEST)); return; } // Allow only GET methods. if (req.getMethod() != HttpMethod.GET) { sendHttpResponse(ctx, req, new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.FORBIDDEN)); return; } // Handshake WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory(getWebSocketLocation(req), "xmpp", false); handshaker = wsFactory.newHandshaker(req); if (handshaker == null) { WebSocketServerHandshakerFactory.sendUnsupportedVersionResponse(ctx.channel()); } else { handshaker.handshake(ctx.channel(), req); } } private void handleWebSocketFrame(ChannelHandlerContext ctx, WebSocketFrame frame) { if (frame instanceof CloseWebSocketFrame) { handshaker.close(ctx.channel(), (CloseWebSocketFrame) frame.retain()); } else if (frame instanceof PingWebSocketFrame) { ctx.channel().write(new PongWebSocketFrame(frame.content().retain())); } else { ctx.fireChannelRead(frame); } } private void sendHttpResponse(ChannelHandlerContext ctx, FullHttpRequest req, FullHttpResponse res) { // Generate an error page if response getStatus code is not OK (200). if (res.getStatus().code() != 200) { ByteBuf buf = Unpooled.copiedBuffer(res.getStatus().toString(), CharsetUtil.UTF_8); res.content().writeBytes(buf); buf.release(); HttpHeaders.setContentLength(res, res.content().readableBytes()); } // Send the response and close the connection if necessary. ChannelFuture f = ctx.channel().writeAndFlush(res); if (!HttpHeaders.isKeepAlive(req) || res.getStatus().code() != 200) { f.addListener(ChannelFutureListener.CLOSE); } } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { cause.printStackTrace(); ctx.close(); } private String getWebSocketLocation(FullHttpRequest req) { String location = req.headers().get(HttpHeaders.Names.HOST); return "ws://" + location; } } public <T> Promise<T> createPromise() { return bootstrap.childGroup().next().newPromise(); } public ClusterClient getCluster() { return cluster; } public void sendMapReduceRequest(MapReduceRequest request) { mapReduce.sendRequest(request); } public void registerCustomMessage(int id, Class<?> clazz) { cluster.getMessageRegistry().addCustomMessage(CUSTOM_MESSAGE_START + id, clazz); } @Override public void onNodeJoin(ClusterClient cluster, NodeId nodeId) { logger.info("Node {} joined cluster", nodeId.toString()); } @Override public void onNodeLeft(ClusterClient cluster, NodeId nodeId) { logger.info("Node {} left cluster", nodeId.toString()); } @Override public void onMessage(ClusterClient cluster, NodeId senderId, Object message) { // inject server reference if (message instanceof ClusterTask) { ((ClusterTask) message).setKixmppServer(this); } if (message instanceof MapReduceRequest) { MapReduceRequest request = (MapReduceRequest) message; request.setSenderId(senderId); getEventEngine().publishTask(request.getTargetJID(),request); } else if (message instanceof MapReduceResponse) { MapReduceResponse response = (MapReduceResponse) message; mapReduce.processResponse(response); } else if (message instanceof RoomTask) { RoomTask roomTask = (RoomTask) message; module(MucKixmppServerModule.class).handleClusterTask(roomTask); } else if (message instanceof Task) { Task task = (Task) message; task.run(); } } }