d4271a57c2571d6a326c14fd930bbbe91d523987
[jFCPlib.git] / src / net / pterodactylus / fcp / FcpConnection.java
1 /*
2  * jSite2 - FpcConnection.java -
3  * Copyright © 2008 David Roden
4  *
5  * This program is free software; you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation; either version 2 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this program; if not, write to the Free Software
17  * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
18  */
19
20 package net.pterodactylus.fcp;
21
22 import java.io.Closeable;
23 import java.io.FilterInputStream;
24 import java.io.IOException;
25 import java.io.InputStream;
26 import java.io.OutputStream;
27 import java.net.InetAddress;
28 import java.net.Socket;
29 import java.net.UnknownHostException;
30 import java.util.Collections;
31 import java.util.HashMap;
32 import java.util.Map;
33 import java.util.logging.Logger;
34
35 /**
36  * An FCP connection to a Freenet node.
37  *
38  * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
39  */
40 public class FcpConnection implements Closeable {
41
42         /** Logger. */
43         private static final Logger logger = Logger.getLogger(FcpConnection.class.getName());
44
45         /** The default port for FCP v2. */
46         public static final int DEFAULT_PORT = 9481;
47
48         /** Listener management. */
49         private final FcpListenerManager fcpListenerManager = new FcpListenerManager(this);
50
51         /** The address of the node. */
52         private final InetAddress address;
53
54         /** The port number of the node’s FCP port. */
55         private final int port;
56
57         /** The remote socket. */
58         private Socket remoteSocket;
59
60         /** The input stream from the node. */
61         private InputStream remoteInputStream;
62
63         /** The output stream to the node. */
64         private OutputStream remoteOutputStream;
65
66         /** The connection handler. */
67         private FcpConnectionHandler connectionHandler;
68
69         /** Incoming message statistics. */
70         private Map<String, Integer> incomingMessageStatistics = Collections.synchronizedMap(new HashMap<String, Integer>());
71
72         /**
73          * Creates a new FCP connection to the freenet node running on localhost,
74          * using the default port.
75          *
76          * @throws UnknownHostException
77          *             if the hostname can not be resolved
78          */
79         public FcpConnection() throws UnknownHostException {
80                 this(InetAddress.getLocalHost());
81         }
82
83         /**
84          * Creates a new FCP connection to the Freenet node running on the given
85          * host, listening on the default port.
86          *
87          * @param host
88          *            The hostname of the Freenet node
89          * @throws UnknownHostException
90          *             if <code>host</code> can not be resolved
91          */
92         public FcpConnection(String host) throws UnknownHostException {
93                 this(host, DEFAULT_PORT);
94         }
95
96         /**
97          * Creates a new FCP connection to the Freenet node running on the given
98          * host, listening on the given port.
99          *
100          * @param host
101          *            The hostname of the Freenet node
102          * @param port
103          *            The port number of the node’s FCP port
104          * @throws UnknownHostException
105          *             if <code>host</code> can not be resolved
106          */
107         public FcpConnection(String host, int port) throws UnknownHostException {
108                 this(InetAddress.getByName(host), port);
109         }
110
111         /**
112          * Creates a new FCP connection to the Freenet node running at the given
113          * address, listening on the default port.
114          *
115          * @param address
116          *            The address of the Freenet node
117          */
118         public FcpConnection(InetAddress address) {
119                 this(address, DEFAULT_PORT);
120         }
121
122         /**
123          * Creates a new FCP connection to the Freenet node running at the given
124          * address, listening on the given port.
125          *
126          * @param address
127          *            The address of the Freenet node
128          * @param port
129          *            The port number of the node’s FCP port
130          */
131         public FcpConnection(InetAddress address, int port) {
132                 this.address = address;
133                 this.port = port;
134         }
135
136         //
137         // LISTENER MANAGEMENT
138         //
139
140         /**
141          * Adds the given listener to the list of listeners.
142          *
143          * @param fcpListener
144          *            The listener to add
145          */
146         public void addFcpListener(FcpListener fcpListener) {
147                 fcpListenerManager.addListener(fcpListener);
148         }
149
150         /**
151          * Removes the given listener from the list of listeners.
152          *
153          * @param fcpListener
154          *            The listener to remove
155          */
156         public void removeFcpListener(FcpListener fcpListener) {
157                 fcpListenerManager.removeListener(fcpListener);
158         }
159
160         //
161         // ACTIONS
162         //
163
164         /**
165          * Connects to the node.
166          *
167          * @throws IOException
168          *             if an I/O error occurs
169          * @throws IllegalStateException
170          *             if there is already a connection to the node
171          */
172         public synchronized void connect() throws IOException, IllegalStateException {
173                 if (connectionHandler != null) {
174                         throw new IllegalStateException("already connected, disconnect first");
175                 }
176                 logger.info("connecting to " + address + ":" + port + "…");
177                 remoteSocket = new Socket(address, port);
178                 remoteInputStream = remoteSocket.getInputStream();
179                 remoteOutputStream = remoteSocket.getOutputStream();
180                 new Thread(connectionHandler = new FcpConnectionHandler(this, remoteInputStream)).start();
181         }
182
183         /**
184          * Disconnects from the node. If there is no connection to the node, this
185          * method does nothing.
186          *
187          * @deprecated Use {@link #close()} instead
188          */
189         @Deprecated
190         public synchronized void disconnect() {
191                 close();
192         }
193
194         /**
195          * Closes the connection. If there is no connection to the node, this method
196          * does nothing.
197          */
198         public void close() {
199                 handleDisconnect(null);
200         }
201
202         /**
203          * Sends the given FCP message.
204          *
205          * @param fcpMessage
206          *            The FCP message to send
207          * @throws IOException
208          *             if an I/O error occurs
209          */
210         public synchronized void sendMessage(FcpMessage fcpMessage) throws IOException {
211                 logger.fine("sending message: " + fcpMessage.getName());
212                 fcpMessage.write(remoteOutputStream);
213         }
214
215         //
216         // PACKAGE-PRIVATE METHODS
217         //
218
219         /**
220          * Handles the given message, notifying listeners. This message should only
221          * be called by {@link FcpConnectionHandler}.
222          *
223          * @param fcpMessage
224          *            The received message
225          */
226         void handleMessage(FcpMessage fcpMessage) {
227                 logger.fine("received message: " + fcpMessage.getName());
228                 String messageName = fcpMessage.getName();
229                 countMessage(messageName);
230                 if ("SimpleProgress".equals(messageName)) {
231                         fcpListenerManager.fireReceivedSimpleProgress(new SimpleProgress(fcpMessage));
232                 } else if ("ProtocolError".equals(messageName)) {
233                         fcpListenerManager.fireReceivedProtocolError(new ProtocolError(fcpMessage));
234                 } else if ("PersistentGet".equals(messageName)) {
235                         fcpListenerManager.fireReceivedPersistentGet(new PersistentGet(fcpMessage));
236                 } else if ("PersistentPut".equals(messageName)) {
237                         fcpListenerManager.fireReceivedPersistentPut(new PersistentPut(fcpMessage));
238                 } else if ("PersistentPutDir".equals(messageName)) {
239                         fcpListenerManager.fireReceivedPersistentPutDir(new PersistentPutDir(fcpMessage));
240                 } else if ("URIGenerated".equals(messageName)) {
241                         fcpListenerManager.fireReceivedURIGenerated(new URIGenerated(fcpMessage));
242                 } else if ("EndListPersistentRequests".equals(messageName)) {
243                         fcpListenerManager.fireReceivedEndListPersistentRequests(new EndListPersistentRequests(fcpMessage));
244                 } else if ("Peer".equals(messageName)) {
245                         fcpListenerManager.fireReceivedPeer(new Peer(fcpMessage));
246                 } else if ("PeerNote".equals(messageName)) {
247                         fcpListenerManager.fireReceivedPeerNote(new PeerNote(fcpMessage));
248                 } else if ("StartedCompression".equals(messageName)) {
249                         fcpListenerManager.fireReceivedStartedCompression(new StartedCompression(fcpMessage));
250                 } else if ("FinishedCompression".equals(messageName)) {
251                         fcpListenerManager.fireReceivedFinishedCompression(new FinishedCompression(fcpMessage));
252                 } else if ("GetFailed".equals(messageName)) {
253                         fcpListenerManager.fireReceivedGetFailed(new GetFailed(fcpMessage));
254                 } else if ("PutFetchable".equals(messageName)) {
255                         fcpListenerManager.fireReceivedPutFetchable(new PutFetchable(fcpMessage));
256                 } else if ("PutSuccessful".equals(messageName)) {
257                         fcpListenerManager.fireReceivedPutSuccessful(new PutSuccessful(fcpMessage));
258                 } else if ("PutFailed".equals(messageName)) {
259                         fcpListenerManager.fireReceivedPutFailed(new PutFailed(fcpMessage));
260                 } else if ("DataFound".equals(messageName)) {
261                         fcpListenerManager.fireReceivedDataFound(new DataFound(fcpMessage));
262                 } else if ("SubscribedUSKUpdate".equals(messageName)) {
263                         fcpListenerManager.fireReceivedSubscribedUSKUpdate(new SubscribedUSKUpdate(fcpMessage));
264                 } else if ("IdentifierCollision".equals(messageName)) {
265                         fcpListenerManager.fireReceivedIdentifierCollision(new IdentifierCollision(fcpMessage));
266                 } else if ("AllData".equals(messageName)) {
267                         LimitedInputStream payloadInputStream = getInputStream(FcpUtils.safeParseLong(fcpMessage.getField("DataLength")));
268                         fcpListenerManager.fireReceivedAllData(new AllData(fcpMessage, payloadInputStream));
269                         try {
270                                 payloadInputStream.consume();
271                         } catch (IOException ioe1) {
272                                 /* well, ignore. when the connection handler fails, all fails. */
273                         }
274                 } else if ("EndListPeerNotes".equals(messageName)) {
275                         fcpListenerManager.fireReceivedEndListPeerNotes(new EndListPeerNotes(fcpMessage));
276                 } else if ("EndListPeers".equals(messageName)) {
277                         fcpListenerManager.fireReceivedEndListPeers(new EndListPeers(fcpMessage));
278                 } else if ("SSKKeypair".equals(messageName)) {
279                         fcpListenerManager.fireReceivedSSKKeypair(new SSKKeypair(fcpMessage));
280                 } else if ("PeerRemoved".equals(messageName)) {
281                         fcpListenerManager.fireReceivedPeerRemoved(new PeerRemoved(fcpMessage));
282                 } else if ("PersistentRequestModified".equals(messageName)) {
283                         fcpListenerManager.fireReceivedPersistentRequestModified(new PersistentRequestModified(fcpMessage));
284                 } else if ("PersistentRequestRemoved".equals(messageName)) {
285                         fcpListenerManager.fireReceivedPersistentRequestRemoved(new PersistentRequestRemoved(fcpMessage));
286                 } else if ("UnknownPeerNoteType".equals(messageName)) {
287                         fcpListenerManager.fireReceivedUnknownPeerNoteType(new UnknownPeerNoteType(fcpMessage));
288                 } else if ("UnknownNodeIdentifier".equals(messageName)) {
289                         fcpListenerManager.fireReceivedUnknownNodeIdentifier(new UnknownNodeIdentifier(fcpMessage));
290                 } else if ("FCPPluginReply".equals(messageName)) {
291                         LimitedInputStream payloadInputStream = getInputStream(FcpUtils.safeParseLong(fcpMessage.getField("DataLength")));
292                         fcpListenerManager.fireReceivedFCPPluginReply(new FCPPluginReply(fcpMessage, payloadInputStream));
293                         try {
294                                 payloadInputStream.consume();
295                         } catch (IOException ioe1) {
296                                 /* ignore. */
297                         }
298                 } else if ("PluginInfo".equals(messageName)) {
299                         fcpListenerManager.fireReceivedPluginInfo(new PluginInfo(fcpMessage));
300                 } else if ("NodeData".equals(messageName)) {
301                         fcpListenerManager.fireReceivedNodeData(new NodeData(fcpMessage));
302                 } else if ("TestDDAReply".equals(messageName)) {
303                         fcpListenerManager.fireReceivedTestDDAReply(new TestDDAReply(fcpMessage));
304                 } else if ("TestDDAComplete".equals(messageName)) {
305                         fcpListenerManager.fireReceivedTestDDAComplete(new TestDDAComplete(fcpMessage));
306                 } else if ("ConfigData".equals(messageName)) {
307                         fcpListenerManager.fireReceivedConfigData(new ConfigData(fcpMessage));
308                 } else if ("NodeHello".equals(messageName)) {
309                         fcpListenerManager.fireReceivedNodeHello(new NodeHello(fcpMessage));
310                 } else if ("CloseConnectionDuplicateClientName".equals(messageName)) {
311                         fcpListenerManager.fireReceivedCloseConnectionDuplicateClientName(new CloseConnectionDuplicateClientName(fcpMessage));
312                 } else if ("ReceivedBookmarkFeed".equals(messageName)) {
313                         fcpListenerManager.fireReceivedBookmarkFeed(new ReceivedBookmarkFeed(fcpMessage));
314                 } else {
315                         fcpListenerManager.fireMessageReceived(fcpMessage);
316                 }
317         }
318
319         /**
320          * Handles a disconnect from the node.
321          *
322          * @param throwable
323          *            The exception that caused the disconnect, or <code>null</code>
324          *            if there was no exception
325          */
326         synchronized void handleDisconnect(Throwable throwable) {
327                 FcpUtils.close(remoteInputStream);
328                 FcpUtils.close(remoteOutputStream);
329                 FcpUtils.close(remoteSocket);
330                 if (connectionHandler != null) {
331                         connectionHandler.stop();
332                         connectionHandler = null;
333                         fcpListenerManager.fireConnectionClosed(throwable);
334                 }
335         }
336
337         //
338         // PRIVATE METHODS
339         //
340
341         /**
342          * Incremets the counter in {@link #incomingMessageStatistics} by
343          * <cod>1</code> for the given message name.
344          *
345          * @param name
346          *            The name of the message to count
347          */
348         private void countMessage(String name) {
349                 int oldValue = 0;
350                 if (incomingMessageStatistics.containsKey(name)) {
351                         oldValue = incomingMessageStatistics.get(name);
352                 }
353                 incomingMessageStatistics.put(name, oldValue + 1);
354                 logger.finest("count for " + name + ": " + (oldValue + 1));
355         }
356
357         /**
358          * Returns a limited input stream from the node’s input stream.
359          *
360          * @param dataLength
361          *            The length of the stream
362          * @return The limited input stream
363          */
364         private synchronized LimitedInputStream getInputStream(long dataLength) {
365                 if (dataLength <= 0) {
366                         return new LimitedInputStream(null, 0);
367                 }
368                 return new LimitedInputStream(remoteInputStream, dataLength);
369         }
370
371         /**
372          * A wrapper around an {@link InputStream} that only supplies a limit number
373          * of bytes from the underlying input stream.
374          *
375          * @author David ‘Bombe’ Roden &lt;bombe@freenetproject.org&gt;
376          */
377         private static class LimitedInputStream extends FilterInputStream {
378
379                 /** The remaining number of bytes that can be read. */
380                 private long remaining;
381
382                 /**
383                  * Creates a new LimitedInputStream that supplies at most
384                  * <code>length</code> bytes from the given input stream.
385                  *
386                  * @param inputStream
387                  *            The input stream
388                  * @param length
389                  *            The number of bytes to read
390                  */
391                 public LimitedInputStream(InputStream inputStream, long length) {
392                         super(inputStream);
393                         remaining = length;
394                 }
395
396                 /**
397                  * @see java.io.FilterInputStream#available()
398                  */
399                 @Override
400                 public synchronized int available() throws IOException {
401                         if (remaining == 0) {
402                                 return 0;
403                         }
404                         return (int) Math.min(super.available(), Math.min(Integer.MAX_VALUE, remaining));
405                 }
406
407                 /**
408                  * @see java.io.FilterInputStream#read()
409                  */
410                 @Override
411                 public synchronized int read() throws IOException {
412                         int read = -1;
413                         if (remaining > 0) {
414                                 read = super.read();
415                                 remaining--;
416                         }
417                         return read;
418                 }
419
420                 /**
421                  * @see java.io.FilterInputStream#read(byte[], int, int)
422                  */
423                 @Override
424                 public synchronized int read(byte[] b, int off, int len) throws IOException {
425                         if (remaining == 0) {
426                                 return -1;
427                         }
428                         int toCopy = (int) Math.min(len, Math.min(remaining, Integer.MAX_VALUE));
429                         int read = super.read(b, off, toCopy);
430                         remaining -= read;
431                         return read;
432                 }
433
434                 /**
435                  * @see java.io.FilterInputStream#skip(long)
436                  */
437                 @Override
438                 public synchronized long skip(long n) throws IOException {
439                         if ((n < 0) || (remaining == 0)) {
440                                 return 0;
441                         }
442                         long skipped = super.skip(Math.min(n, remaining));
443                         remaining -= skipped;
444                         return skipped;
445                 }
446
447                 /**
448                  * {@inheritDoc} This method does nothing, as {@link #mark(int)} and
449                  * {@link #reset()} are not supported.
450                  *
451                  * @see java.io.FilterInputStream#mark(int)
452                  */
453                 @Override
454                 public void mark(int readlimit) {
455                         /* do nothing. */
456                 }
457
458                 /**
459                  * {@inheritDoc}
460                  *
461                  * @see java.io.FilterInputStream#markSupported()
462                  * @return <code>false</code>
463                  */
464                 @Override
465                 public boolean markSupported() {
466                         return false;
467                 }
468
469                 /**
470                  * {@inheritDoc} This method does nothing, as {@link #mark(int)} and
471                  * {@link #reset()} are not supported.
472                  *
473                  * @see java.io.FilterInputStream#reset()
474                  */
475                 @Override
476                 public void reset() throws IOException {
477                         /* do nothing. */
478                 }
479
480                 /**
481                  * Consumes the input stream, i.e. read all bytes until the limit is
482                  * reached.
483                  *
484                  * @throws IOException
485                  *             if an I/O error occurs
486                  */
487                 public synchronized void consume() throws IOException {
488                         while (remaining > 0) {
489                                 skip(remaining);
490                         }
491                 }
492
493         }
494
495 }