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