/* * Copyright 2013 Thomas Bocek * * 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 net.tomp2p.connection2; import java.util.Collections; import java.util.HashMap; import java.util.Map; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; import io.netty.channel.ChannelHandler.Sharable; import io.netty.channel.socket.DatagramChannel; import net.tomp2p.message.Message2; import net.tomp2p.message.Message2.Type; import net.tomp2p.peers.Number160; import net.tomp2p.peers.PeerAddress; import net.tomp2p.peers.PeerStatusListener; import net.tomp2p.rpc.DispatchHandler; /** * Used to deliver incoming REQUEST messages to their specific handlers. You can register handlers using the * {@link registerIoHandler} function. * <p> * You probably want to add an instance of this class to the end of a pipeline to be able to receive messages. This * class is able to cover several channels but only one P2P network! * </p> * * @author Thomas Bocek */ @Sharable public class Dispatcher extends SimpleChannelInboundHandler<Message2> { private static final Logger LOG = LoggerFactory.getLogger(Dispatcher.class); private final int p2pID; private final PeerBean peerBean; private volatile Map<Number160, Map<Integer, DispatchHandler>> ioHandlers = new HashMap<Number160, Map<Integer, DispatchHandler>>(); /** * Constructor. * * @param p2pID * the p2p ID the dispatcher is looking for in messages * @param peerBean * . */ public Dispatcher(final int p2pID, final PeerBean peerBean) { this.p2pID = p2pID; this.peerBean = peerBean; } /** * Registers a handler with this dispatcher. Future received messages adhering to the given parameters will be * forwarded to that handler. Note that the dispatcher only handles REQUEST messages. This method is thread-safe, * and uses copy on write as its expected to run this only during initialization. * * @param peerId * Specifies the receiver the dispatcher filters for. This allows to use one dispatcher for several * interfaces or even nodes. * @param ioHandler * the handler which should process the given type of messages * @param names * The command of the {@link Message} the given handler processes. All messages having that command will * be forwarded to the given handler.<br /> * <b>Note:</b> If you register multiple handlers with the same command, only the last registered handler * will receive these messages! */ public void registerIoHandler(final Number160 peerId, final DispatchHandler ioHandler, final int... names) { Map<Number160, Map<Integer, DispatchHandler>> copy = ioHandlers == null ? new HashMap<Number160, Map<Integer, DispatchHandler>>() : new HashMap<Number160, Map<Integer, DispatchHandler>>(ioHandlers); Map<Integer, DispatchHandler> types = copy.get(peerId); if (types == null) { types = new HashMap<Integer, DispatchHandler>(); copy.put(peerId, types); } for (Integer name : names) { types.put(name, ioHandler); } ioHandlers = Collections.unmodifiableMap(copy); } /** * If we shutdown, we remove the handlers. This means that a server may respond that the handler is unknown. * * @param peerId * The Id of the peer to remove the handlers . */ public void removeIoHandler(final Number160 peerId) { if (ioHandlers == null) { return; } Map<Number160, Map<Integer, DispatchHandler>> copy = ioHandlers == null ? new HashMap<Number160, Map<Integer, DispatchHandler>>() : new HashMap<Number160, Map<Integer, DispatchHandler>>(ioHandlers); copy.remove(peerId); ioHandlers = Collections.unmodifiableMap(copy); } @Override protected void channelRead0(final ChannelHandlerContext ctx, final Message2 message) throws Exception { LOG.debug("received request {}", message); if (message.getVersion() != p2pID) { LOG.error("Wrong version. We are looking for {} but we got {}, received: {}", p2pID, message.getVersion(), message); ctx.close(); for (PeerStatusListener peerStatusListener : peerBean.peerStatusListeners()) { peerStatusListener.peerFailed(message.getSender(), true); } return; } Message2 responseMessage = null; final DispatchHandler myHandler = getAssociatedHandler(message); if (myHandler != null) { LOG.debug("about to respond to {}", message); responseMessage = myHandler.forwardMessage(message); if (responseMessage == null) { LOG.warn("Repsonse message was null, probaly a custom handler failed {}", message); message.setRecipient(message.getSender()).setSender(peerBean.serverPeerAddress()) .setType(Type.EXCEPTION); response(ctx, message); } else if (responseMessage == message) { LOG.debug("The reply handler was a fire-and-forget handler, we don't send any message back! ", message); ctx.close(); } else { response(ctx, responseMessage); } } else { LOG.warn("No handler found for {}. Probably we have shutdown this peer.", message); message.setRecipient(message.getSender()).setSender(peerBean.serverPeerAddress()) .setType(Type.UNKNOWN_ID); response(ctx, message); } } /** * Respond within a session. Keep the connection open if we are asked to do so. Connection is only kept alive for * TCP data. * * @param ctx * The channel context * @param response * The response to send */ private void response(final ChannelHandlerContext ctx, final Message2 response) { if (ctx.channel() instanceof DatagramChannel) { // check if channel is still open. If its not, then do not send // anything because // this will cause an exception that will be logged. if (!ctx.channel().isOpen()) { LOG.debug("channel UDP is not open, do not reply {}", response); return; } LOG.debug("reply UDP message {}", response); ctx.channel().writeAndFlush(response); } else { // check if channel is still open. If its not, then do not send // anything because // this will cause an exception that will be logged. if (!ctx.channel().isActive()) { LOG.debug("channel TCP is not open, do not reply {}", response); return; } LOG.debug("reply TCP message {} to {}", response, ctx.channel().remoteAddress()); ctx.channel().writeAndFlush(response); } } /** * Checks if we have a handler for the given message. * * @param message * the message a handler should be found for * @return the handler for the given message, null if none has been registered for that message. */ private DispatchHandler getAssociatedHandler(final Message2 message) { if (message == null || !(message.isRequest())) { return null; } PeerAddress recipient = message.getRecipient(); // Search for handler, 0 is ping if (recipient.getPeerId().isZero() && message.getCommand() == 0) { return searchHandler(peerBean.serverPeerAddress().getPeerId(), 0); } else { return searchHandler(recipient.getPeerId(), Integer.valueOf(message.getCommand())); } } /** * Looks for a registered handler according to the given parameters. * * @param recipientID * The recipient of the message * @param command * The type of the message to be filtered * @return the handler for the given message or null if none has been found */ private DispatchHandler searchHandler(final Number160 recipientID, final Integer command) { if (ioHandlers == null) { return null; } Map<Integer, DispatchHandler> types = ioHandlers.get(recipientID); if (types != null && types.containsKey(command)) { return types.get(command); } else { // not registered LOG.debug("Handler not found for type {} we are looking for the server with ID {}", command, recipientID); return null; } } }