/* * Mojito Distributed Hash Table (Mojito DHT) * Copyright (C) 2006-2007 LimeWire LLC * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ package org.limewire.mojito.io; import java.io.IOException; import java.net.SocketAddress; import java.nio.ByteBuffer; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.limewire.io.NetworkUtils; import org.limewire.mojito.Context; import org.limewire.mojito.KUID; import org.limewire.mojito.exceptions.IllegalSocketAddressException; import org.limewire.mojito.handler.DefaultMessageHandler; import org.limewire.mojito.handler.RequestHandler; import org.limewire.mojito.handler.ResponseHandler; import org.limewire.mojito.handler.request.FindNodeRequestHandler; import org.limewire.mojito.handler.request.FindValueRequestHandler; import org.limewire.mojito.handler.request.PingRequestHandler; import org.limewire.mojito.handler.request.StatsRequestHandler; import org.limewire.mojito.handler.request.StoreRequestHandler; import org.limewire.mojito.io.MessageDispatcher.MessageDispatcherEvent.EventType; import org.limewire.mojito.io.Tag.Receipt; import org.limewire.mojito.messages.DHTMessage; import org.limewire.mojito.messages.DHTSecureMessage; import org.limewire.mojito.messages.FindNodeRequest; import org.limewire.mojito.messages.FindNodeResponse; import org.limewire.mojito.messages.FindValueRequest; import org.limewire.mojito.messages.FindValueResponse; import org.limewire.mojito.messages.MessageFormatException; import org.limewire.mojito.messages.MessageID; import org.limewire.mojito.messages.PingRequest; import org.limewire.mojito.messages.PingResponse; import org.limewire.mojito.messages.RequestMessage; import org.limewire.mojito.messages.ResponseMessage; import org.limewire.mojito.messages.StatsRequest; import org.limewire.mojito.messages.StatsResponse; import org.limewire.mojito.messages.StoreRequest; import org.limewire.mojito.messages.StoreResponse; import org.limewire.mojito.routing.Contact; import org.limewire.mojito.settings.NetworkSettings; import org.limewire.mojito.util.ContactUtils; import org.limewire.mojito.util.FixedSizeHashMap; import org.limewire.mojito.util.HostFilter; import org.limewire.mojito.util.MessageUtils; import org.limewire.security.SecureMessage; import org.limewire.security.SecureMessageCallback; import org.limewire.util.StringUtils; /** * An abstract class that takes care of all Mojito's communication needs. */ public abstract class MessageDispatcher { private static final Log LOG = LogFactory.getLog(MessageDispatcher.class); /** * The maximum size of a serialized Message we can send. */ private static final int MAX_MESSAGE_SIZE = NetworkSettings.MAX_MESSAGE_SIZE.getValue(); /** Map of Messages (responses) we're awaiting. */ private final ReceiptMap receiptMap = new ReceiptMap(512); /** Handle of the Context. */ protected final Context context; private final DefaultMessageHandler defaultHandler; private final PingRequestHandler pingHandler; private final FindNodeRequestHandler findNodeHandler; private final FindValueRequestHandler findValueHandler; private final StoreRequestHandler storeHandler; private final StatsRequestHandler statsHandler; /** * Handle of the cleanup task future. */ private ScheduledFuture cleanupTaskFuture; /** A list of MessageDispatcherListeners */ private final List<MessageDispatcherListener> listeners = new CopyOnWriteArrayList<MessageDispatcherListener>(); public MessageDispatcher(Context context) { this.context = context; defaultHandler = new DefaultMessageHandler(context); pingHandler = new PingRequestHandler(context); findNodeHandler = new FindNodeRequestHandler(context); findValueHandler = new FindValueRequestHandler(context, findNodeHandler); storeHandler = new StoreRequestHandler(context); statsHandler = new StatsRequestHandler(context); } /** * Adds a MessageDispatcherListener. * <p> * Implementation Note: The listener(s) is not called from a * separate event Thread! That means processor intensive tasks * that are performed straight in the listener(s) can slowdown * the processing throughput significantly. Offload intensive * tasks to separate Threads in necessary! * * @param l the MessageDispatcherListener instance to add */ public void addMessageDispatcherListener(MessageDispatcherListener l) { if (l == null) { throw new NullPointerException("MessageDispatcherListener is null"); } listeners.add(l); } /** * Removes a <code>MessageDispatcherListener</code>. * * @param l the MessageDispatcherListener instance to remove */ public void removeMessageDispatcherListener(MessageDispatcherListener l) { if (l == null) { throw new NullPointerException("MessageDispatcherListener is null"); } listeners.remove(l); } /** * Binds the DatagramSocket to the given SocketAddress. */ public abstract void bind(SocketAddress address) throws IOException; /** * Starts the MessageDispatcher. */ public void start() { // Start the CleanupTask synchronized (receiptMap) { if (cleanupTaskFuture == null) { long delay = NetworkSettings.CLEANUP_RECEIPTS_DELAY.getValue(); Runnable task = new Runnable() { public void run() { process(new CleanupProcessor()); } }; cleanupTaskFuture = context.getDHTExecutorService().scheduleWithFixedDelay( task, 0L, delay, TimeUnit.MILLISECONDS); } } } /** * Stops the MessageDispatcher. */ public void stop() { // Stop the CleanupTask synchronized (receiptMap) { if (cleanupTaskFuture != null) { cleanupTaskFuture.cancel(true); cleanupTaskFuture = null; } } } /** * Closes the MessageDispatcher and releases all resources. */ public void close() { stop(); clear(); } /** * Returns whether or not incoming Requests or Responses * are accepted. The default implementation returns true. */ public boolean isAccepting() { return true; } /** * Returns whether or not the MessageDispatcher is bound to a Socket. */ public abstract boolean isBound(); /** * Returns whether or not the MessageDispatcher is running. */ public abstract boolean isRunning(); /** * Sends a ResponseMessage to the given Contact. */ public boolean send(Contact contact, ResponseMessage response) throws IOException { return send(new Tag(contact, response)); } /** * Sends a RequestMessage to the given SocketAddress and registers * a ResponseHandler. */ public boolean send(SocketAddress dst, RequestMessage request, ResponseHandler responseHandler) throws IOException { return send(new Tag(dst, request, responseHandler)); } /** * Sends a RequestMessage to the given SocketAddress and registers * a ResponseHandler. */ public boolean send(KUID nodeId, SocketAddress dst, RequestMessage request, ResponseHandler responseHandler) throws IOException { return send(new Tag(nodeId, dst, request, responseHandler)); } /** * Sends a RequestMessage to the given Contact and registers * a ResponseHandler. */ public boolean send(Contact contact, RequestMessage request, ResponseHandler responseHandler) throws IOException { return send(new Tag(contact, request, responseHandler)); } /** * The actual send method. */ protected boolean send(final Tag tag) throws IOException { if (!isRunning()) { throw new IOException("Cannot send Message because MessageDispatcher is not running"); } KUID nodeId = tag.getNodeID(); SocketAddress dst = tag.getSocketAddress(); DHTMessage message = tag.getMessage(); // Make sure we're not sending messages to ourself if (context.isLocalContactAddress(dst)) { // This can happen when the RouteTable was // initialized with Nodes from an external // source like a file. It's not really an // error because the Node's ID is different // but there's no point in sending the Msg. String msg = "Cannot send Message of type " + message.getClass().getName() + " to " + ContactUtils.toString(nodeId, dst) + " because it has the same contact address as the local Node " + context.getLocalNode() + " has"; if (LOG.isInfoEnabled()) { LOG.info(msg); } process(new ErrorProcessor(tag, new IOException(msg))); return false; } // Like above. It makes no sense to send messages to a // Node that has our local Node ID but we have to permit // this case for ID collision test pings if (context.isLocalNodeID(nodeId) && !MessageUtils.isCollisionPingRequest( context.getLocalNodeID(), message)) { String msg = "Cannot send Message of type " + message.getClass().getName() + " to " + ContactUtils.toString(nodeId, dst) + " which is equal to our local Node " + context.getLocalNode(); if (LOG.isErrorEnabled()) { LOG.error(msg); } process(new ErrorProcessor(tag, new IOException(msg))); return false; } // Check if it's a valid destination address if (!NetworkUtils.isValidSocketAddress(dst)) { String msg = "Invalid IP:Port " + ContactUtils.toString(nodeId, dst); if (LOG.isErrorEnabled()) { LOG.error(msg); } process(new ErrorProcessor(tag, new IllegalSocketAddressException(msg))); return false; } // And make sure we're not sending messages to private // IPs if it's not permitted. Two Nodes behind the same // NAT using private IPs to communicate with each other // would screw up a few things. if (ContactUtils.isPrivateAddress(dst)) { String msg = "Private IP:Port " + ContactUtils.toString(nodeId, dst); if (LOG.isErrorEnabled()) { LOG.error(msg); } process(new ErrorProcessor(tag, new IllegalSocketAddressException(msg))); return false; } // Serialize the Message ByteBuffer data = serialize(dst, message); int size = data.remaining(); if (size <= 0) { process(new ErrorProcessor(tag, new IOException("Illegal Message size: " + size))); return false; } else if (size >= MAX_MESSAGE_SIZE) { if (LOG.isWarnEnabled()) { LOG.warn("Message (" + message.getClass().getName() + ") is too large: " + size + " >= " + MAX_MESSAGE_SIZE); } } tag.setData(data); process(new SubmitProcessor(tag)); return true; } /** * Enqueues Tag to the Output queue. */ protected abstract boolean submit(Tag tag); /** * A helper method to serialize DHTMessage(s). */ protected ByteBuffer serialize(SocketAddress dst, DHTMessage message) throws IOException { return context.getMessageFactory().writeMessage(dst, message); } /** * A helper method to deserialize DHTMessage(s). */ protected DHTMessage deserialize(SocketAddress src, ByteBuffer data) throws MessageFormatException, IOException { return context.getMessageFactory().createMessage(src, data); } /** * Handles a DHTMessage as read from Network. */ protected void handleMessage(DHTMessage message) { if (!isAccepting()) { if (LOG.isDebugEnabled()) { LOG.debug("Dropping " + message + " because MessageDispatcher is " + "not accepting requests nor responses"); } return; } // Make sure we're not receiving messages from ourself. Contact node = message.getContact(); KUID nodeId = node.getNodeID(); SocketAddress src = node.getContactAddress(); MessageID messageId = message.getMessageID(); if (context.isLocalContactAddress(src) || (context.isLocalNodeID(nodeId) && !(message instanceof PingResponse))) { if (LOG.isErrorEnabled()) { String msg = "Received a message of type " + message.getClass().getName() + " from " + node + " which is equal to our local Node " + context.getLocalNode(); LOG.error(msg); } return; } if (!NetworkUtils.isValidSocketAddress(src)) { if (LOG.isErrorEnabled()) { LOG.error(node + " has an invalid IP:Port"); } return; } // Make sure we're not mixing IPv4 and IPv6 addresses. // See RouteTableImpl.add() for more info! if (!ContactUtils.isSameAddressSpace(context.getLocalNode(), node)) { // Log as ERROR so that we're not missing this if (LOG.isErrorEnabled()) { LOG.error(node + " is from a different IP address space than local Node"); } return; } fireMessageReceived(message); if (!accept(message)) { if (LOG.isInfoEnabled()) { LOG.info("Dropping message from " + node); } fireMessageFiltered(message); return; } if (message instanceof ResponseMessage) { ResponseMessage response = (ResponseMessage)message; // The remote Node thinks it's firewalled but it responded // for some odd reason which it shouldn't regardless if it // is really firewalled (it didn't receive our request in // first place) or pretends to be firewalled in which is // a hint that it doesn't want to be contacted. Anyways, it // is a bug on the other side (a Mojito compatible impl). if (node.isFirewalled() && NetworkSettings.DROP_RESPONE_IF_FIREWALLED.getValue()) { if (LOG.isErrorEnabled()) { LOG.error("Dropping " + message + " because sender is firewalled"); } return; } // Check the SecurityToken in the MessageID to figure out // whether or not we have ever sent a Request to that Host! if (messageId.isTaggingSupported() && !messageId.isFor(src)) { if (LOG.isWarnEnabled()) { LOG.warn(response.getContact() + " sent us an unsolicited response: " + response); } return; } Receipt receipt = null; synchronized(receiptMap) { receipt = receiptMap.get(messageId); if (receipt != null) { receipt.received(); // The SecurityToken check should catch all malicious // and some buggy Nodes. Do some additional sanity // checks to make sure the NodeID, IP:Port and // response type have the expected values. if (!receipt.sanityCheck(response)) { if (LOG.isWarnEnabled()) { LOG.warn("Response from " + response.getContact() + " did not pass the sanity check"); } return; } // Set the Round Trip Time (RTT) message.getContact().setRoundTripTime(receipt.time()); // OK, all checks passed. We can remove the receipt now! receiptMap.remove(messageId); } } // If it's a late response (the Receipt got evicted by the // cleanup task) and tagging of MessageIDs is NOT supported // than exit here as we're not able to verify if we've ever // sent a request to the given Node. In other words it's an // unsolicited response from our perspective! if (receipt == null && !messageId.isTaggingSupported()) { if (LOG.isTraceEnabled()) { LOG.trace(response.getContact() + " sent us an unsolicited response: " + response); } return; } processResponse(receipt, response); } else if (message instanceof RequestMessage) { // A Node that is marked as firewalled must not respond // to REQUESTS! if (context.getLocalNode().isFirewalled() && NetworkSettings.DROP_REQUEST_IF_FIREWALLED.getValue()) { if (LOG.isInfoEnabled()) { LOG.info("Local Node is firewalled, dropping " + message); } } else { processRequest((RequestMessage)message); } } else { throw new IllegalArgumentException(message + " is neither instance of RequestMessage nor ResponseMessage!"); } } /** * Starts a new ResponseProcessor. */ private void processResponse(Receipt receipt, ResponseMessage response) { ResponseProcessor processor = new ResponseProcessor(receipt, response); if (response instanceof DHTSecureMessage) { verify((DHTSecureMessage)response, processor); } else { process(processor); } } /** * Starts a new RequestProcessor. */ private void processRequest(RequestMessage request) { RequestProcessor processor = new RequestProcessor(request); if (request instanceof DHTSecureMessage) { verify((DHTSecureMessage)request, processor); } else { process(processor); } } /** * Called right after a Message has been sent to register * its ResponseHandler (if it's a RequestMessage). */ protected void register(Tag tag) { Receipt receipt = tag.receipt(); if (receipt != null) { process(new RegisterProcessor(receipt)); } fireMessageSent(tag.getNodeID(), tag.getSocketAddress(), tag.getMessage()); } /** Called to process a Task */ protected abstract void process(Runnable runnable); /** Called to verify a SecureMessage */ protected abstract void verify(SecureMessage secureMessage, SecureMessageCallback smc); /** * This method is called from the MessageDispatcher Thread to * determinate whether or not the DHTMessage should be accepted. * As it's running on the MessageDispatcher Thread it shouldn't * block for too long. Use the allow() method for heavyweight * checks! */ protected boolean accept(DHTMessage message) { return true; } /** * This method is called to determinate whether or not the * DHTMessage should be processed. */ protected boolean allow(DHTMessage message) { HostFilter hostFilter = context.getHostFilter(); if (hostFilter != null) { return hostFilter.allow(message.getContact().getContactAddress()); } return true; } /** * Clears the output queue and receipt map. */ protected void clear() { synchronized (receiptMap) { receiptMap.clear(); } } protected void handleError(Tag tag, IOException err) { process(new ErrorProcessor(tag, err)); } protected void fireMessageSent(KUID nodeId, SocketAddress dst, DHTMessage message) { fireMessageDispatcherEvent(nodeId, dst, message, EventType.MESSAGE_SENT); } protected void fireMessageReceived(DHTMessage message) { fireMessageDispatcherEvent(null, null, message, EventType.MESSAGE_RECEIVED); } protected void fireMessageFiltered(DHTMessage message) { fireMessageDispatcherEvent(null, null, message, EventType.MESSAGE_FILTERED); } protected void fireLateResponse(DHTMessage message) { fireMessageDispatcherEvent(null, null, message, EventType.LATE_RESPONSE); } protected void fireReceiptTimeout(Receipt receipt) { fireMessageDispatcherEvent(receipt.getNodeID(), receipt.getSocketAddress(), receipt.getRequestMessage(), EventType.RECEIPT_TIMEOUT); } protected void fireReceiptEvicted(Receipt receipt) { fireMessageDispatcherEvent(receipt.getNodeID(), receipt.getSocketAddress(), receipt.getRequestMessage(), EventType.RECEIPT_EVICTED); } protected void fireMessageDispatcherEvent(KUID nodeId, SocketAddress dst, DHTMessage message, EventType type) { if (listeners.isEmpty()) { return; } MessageDispatcherEvent evt = new MessageDispatcherEvent( this, nodeId, dst, message, type); for (MessageDispatcherListener listener : listeners) { listener.handleMessageDispatcherEvent(evt); } } /** * A map of MessageID -> Receipts. */ @SuppressWarnings("serial") private class ReceiptMap extends FixedSizeHashMap<MessageID, Receipt> { public ReceiptMap(int maxSize) { super(maxSize); } public void add(Receipt receipt) { put(receipt.getMessageID(), receipt); } /** * Cleans up the Map and kicks off Ticks. Meant to be * called by a scheduled task. */ public void cleanup() { for(Iterator<Receipt> it = values().iterator(); it.hasNext(); ) { Receipt receipt = it.next(); if (receipt.isCancelled()) { // The user cancelled the Future it.remove(); } else if (receipt.timeout()) { receipt.received(); it.remove(); process(new TimeoutProcessor(receipt, true)); } else { process(new TickProcessor(receipt)); } } } @Override protected boolean removeEldestEntry(Map.Entry<MessageID, Receipt> eldest) { Receipt receipt = eldest.getValue(); boolean timeout = receipt.timeout(); if (super.removeEldestEntry(eldest) || timeout) { receipt.received(); process(new TimeoutProcessor(receipt, timeout)); return true; } return false; } } /** * Calls submit(Tag) from the processor Thread. */ private class SubmitProcessor implements Runnable { private final Tag tag; private SubmitProcessor(Tag tag) { this.tag = tag; } public void run() { submit(tag); } } /** * Calls ReceiptMap.add(Receipt) from the processor Thread. */ private class RegisterProcessor implements Runnable { private final Receipt receipt; private RegisterProcessor(Receipt receipt) { this.receipt = receipt; } public void run() { synchronized (receiptMap) { receiptMap.add(receipt); } } } /** * Calls ReceiptMap.cleanup() from the processor Thread. */ private class CleanupProcessor implements Runnable { public void run() { synchronized (receiptMap) { receiptMap.cleanup(); } } } /** * An implementation of Runnable to handle Response Messages. */ private class ResponseProcessor implements Runnable, SecureMessageCallback { private final Receipt receipt; private final ResponseMessage response; private ResponseProcessor(Receipt receipt, ResponseMessage response) { this.receipt = receipt; this.response = response; } public void handleSecureMessage(SecureMessage sm, boolean passed) { if (passed) { process(this); } else if (LOG.isErrorEnabled()) { LOG.error(response.getContact() + " send us a secure response Message but the signatures do not match!"); } } public void run() { if (receipt != null) { processResponse(); } else { processLateResponse(); } } /** * Processes a regular response. */ private void processResponse() { try { defaultHandler.handleResponse(response, receipt.time()); receipt.getResponseHandler().handleResponse(response, receipt.time()); } catch (IOException e) { receipt.handleError(e); LOG.error("An error occured dusring processing the response", e); } } /** * A late response is a response that arrived after a timeout. * We rely on the fact that MessageIDs are tagged with a AddressSecurityToken * so that we can still figure out if we've ever send a request * to the remote Node. * <p> * The fact that the remote Node respond is a valuable information * and we'll use a higher timeout next time. */ private void processLateResponse() { Contact node = response.getContact(); if (LOG.isTraceEnabled()) { if (response instanceof PingResponse) { LOG.trace("Received a late Pong from " + node); } else if (response instanceof FindNodeResponse) { LOG.trace("Received a late FindNode response from " + node); } else if (response instanceof FindValueResponse) { LOG.trace("Received a late FindValue response from " + node); } else if (response instanceof StoreResponse) { LOG.trace("Received a late Store response from " + node); } else if (response instanceof StatsResponse) { LOG.trace("Received a late Stats response from " + node); } } fireLateResponse(response); defaultHandler.handleLateResponse(response); } } /** * An implementation of Runnable to handle Request Messages. */ private class RequestProcessor implements Runnable, SecureMessageCallback { private final RequestMessage request; private RequestProcessor(RequestMessage request) { this.request = request; } public void handleSecureMessage(SecureMessage sm, boolean passed) { if (passed) { process(this); } else if (LOG.isErrorEnabled()) { LOG.error(request.getContact() + " send us a secure request Message but the signatures do not match!"); } } public void run() { // Make sure a singe Node cannot monopolize our resources if (!allow(request)) { if (LOG.isTraceEnabled()) { LOG.trace(request.getContact() + " refused"); } fireMessageFiltered(request); return; } RequestHandler requestHandler = null; if (request instanceof PingRequest) { requestHandler = pingHandler; } else if (request instanceof FindNodeRequest) { requestHandler = findNodeHandler; } else if (request instanceof FindValueRequest) { requestHandler = findValueHandler; } else if (request instanceof StoreRequest) { requestHandler = storeHandler; } else if (request instanceof StatsRequest) { requestHandler = statsHandler; } else { if (LOG.isDebugEnabled()) { LOG.debug("There is no handler for " + request); } } if (requestHandler != null) { try { // Call the default handler after the custom handler // for two reasons: The first reason is security, the // custom handler can do message specific checks and // can throw an Exception if something is fishy and the // second reason is a small optimization (return under // certain conditions k-closest Nodes without the // requester). requestHandler.handleRequest(request); defaultHandler.handleRequest(request); } catch (IOException e) { LOG.error("An exception occured during processing the request", e); } } } } /** * An implementation of Runnable to handle Timeouts. The eviction * of ResponseHandlers (we send too many requests and we hit the * ReceiptMap limit) is also treated as a timeout. */ private class TimeoutProcessor implements Runnable { private final Receipt receipt; private final boolean timeout; private TimeoutProcessor(Receipt receipt, boolean timeout) { this.receipt = receipt; this.timeout = timeout; } public void run() { if (timeout) { fireReceiptTimeout(receipt); } else { fireReceiptEvicted(receipt); } try { KUID nodeId = receipt.getNodeID(); SocketAddress dst = receipt.getSocketAddress(); RequestMessage msg = receipt.getRequestMessage(); long time = receipt.time(); defaultHandler.handleTimeout(nodeId, dst, msg, time); receipt.getResponseHandler().handleTimeout(nodeId, dst, msg, time); } catch (IOException e) { receipt.handleError(e); LOG.error("ReceiptMap removeEldestEntry error: ", e); } } } /** * An implementation of Runnable to handle Ticks. */ private static class TickProcessor implements Runnable { private final Receipt receipt; private TickProcessor(Receipt receipt) { this.receipt = receipt; } public void run() { receipt.handleTick(); } } /** * An implementation of Runnable to handle Errors. */ private static class ErrorProcessor implements Runnable { private final Tag tag; private final IOException exception; private ErrorProcessor(Tag tag, IOException exception) { this.tag = tag; this.exception = exception; } public void run() { tag.handleError(exception); } } /** * The MessageDispatcherListener is called for every send or * received Message. */ public static interface MessageDispatcherListener { /** * Invoked when an event occurs. * * @param evt the event that occurred */ public void handleMessageDispatcherEvent(MessageDispatcherEvent evt); } /** * MessageDispatcherEvent are created and fired for various MessageDispatcher events. */ public static class MessageDispatcherEvent { public static enum EventType { /** Fired if a DHTMessage was send */ MESSAGE_SENT, /** Fired if a DHTMessage was received */ MESSAGE_RECEIVED, /** Fired if a DHTMessage filtered */ MESSAGE_FILTERED, /** Fired if a request timed out */ RECEIPT_TIMEOUT, /** Fired if a request was evicted */ RECEIPT_EVICTED, /** Fired if a late response was received */ LATE_RESPONSE; } private MessageDispatcher messageDispatcher; private KUID nodeId; private SocketAddress dst; private DHTMessage message; private EventType type; public MessageDispatcherEvent(MessageDispatcher messageDispatcher, KUID nodeId, SocketAddress dst, DHTMessage message, EventType type) { this.nodeId = nodeId; this.dst = dst; this.message = message; this.type = type; } public MessageDispatcher getMessageDispatcher() { return messageDispatcher; } public KUID getNodeID() { return nodeId; } public SocketAddress getSocketAddress() { return dst; } public DHTMessage getMessage() { return message; } public EventType getEventType() { return type; } @Override public String toString() { return StringUtils.toString(this, nodeId, dst, message, type); } } }