Update license to GPLv3, fix header comments
[jFCPlib.git] / src / main / java / net / pterodactylus / fcp / FcpConnection.java
1 /*
2  * jFCPlib - FcpConnection.java - Copyright © 2008–2016 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 3 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, see <http://www.gnu.org/licenses/>.
16  */
17
18 package net.pterodactylus.fcp;
19
20 import java.io.Closeable;
21 import java.io.FilterInputStream;
22 import java.io.IOException;
23 import java.io.InputStream;
24 import java.io.OutputStream;
25 import java.net.InetAddress;
26 import java.net.Socket;
27 import java.net.UnknownHostException;
28 import java.util.Collections;
29 import java.util.HashMap;
30 import java.util.Map;
31 import java.util.logging.Logger;
32
33 import net.pterodactylus.fcp.FcpUtils.TempInputStream;
34
35 /**
36  * An FCP connection to a Freenet node.
37  *
38  * @author David ‘Bombe’ Roden &lt;bombe@freenetproject.org&gt;
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 static final 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         public synchronized boolean isClosed() {
161                 return connectionHandler == null;
162         }
163
164         //
165         // ACTIONS
166         //
167
168         /**
169          * Connects to the node.
170          *
171          * @throws IOException
172          *             if an I/O error occurs
173          * @throws IllegalStateException
174          *             if there is already a connection to the node
175          */
176         public synchronized void connect() throws IOException, IllegalStateException {
177                 if (connectionHandler != null) {
178                         throw new IllegalStateException("already connected, disconnect first");
179                 }
180                 logger.info("connecting to " + address + ":" + port + "…");
181                 remoteSocket = new Socket(address, port);
182                 remoteInputStream = remoteSocket.getInputStream();
183                 remoteOutputStream = remoteSocket.getOutputStream();
184                 new Thread(connectionHandler = new FcpConnectionHandler(this, remoteInputStream)).start();
185         }
186
187         /**
188          * Disconnects from the node. If there is no connection to the node, this
189          * method does nothing.
190          *
191          * @deprecated Use {@link #close()} instead
192          */
193         @Deprecated
194         public synchronized void disconnect() {
195                 close();
196         }
197
198         /**
199          * Closes the connection. If there is no connection to the node, this
200          * method does nothing.
201          */
202         @Override
203         public void close() {
204                 handleDisconnect(null);
205         }
206
207         /**
208          * Sends the given FCP message.
209          *
210          * @param fcpMessage
211          *            The FCP message to send
212          * @throws IOException
213          *             if an I/O error occurs
214          */
215         public synchronized void sendMessage(FcpMessage fcpMessage) throws IOException {
216                 logger.fine("sending message: " + fcpMessage.getName());
217                 fcpMessage.write(remoteOutputStream);
218         }
219
220         //
221         // PACKAGE-PRIVATE METHODS
222         //
223
224         /**
225          * Handles the given message, notifying listeners. This message should only
226          * be called by {@link FcpConnectionHandler}.
227          *
228          * @param fcpMessage
229          *            The received message
230          */
231         void handleMessage(FcpMessage fcpMessage) throws IOException{
232                 logger.fine("received message: " + fcpMessage.getName());
233                 String messageName = fcpMessage.getName();
234                 countMessage(messageName);
235                 if ("SimpleProgress".equals(messageName)) {
236                         fcpListenerManager.fireReceivedSimpleProgress(new SimpleProgress(fcpMessage));
237                 } else if ("ProtocolError".equals(messageName)) {
238                         fcpListenerManager.fireReceivedProtocolError(new ProtocolError(fcpMessage));
239                 } else if ("PersistentGet".equals(messageName)) {
240                         fcpListenerManager.fireReceivedPersistentGet(new PersistentGet(fcpMessage));
241                 } else if ("PersistentPut".equals(messageName)) {
242                         fcpListenerManager.fireReceivedPersistentPut(new PersistentPut(fcpMessage));
243                 } else if ("PersistentPutDir".equals(messageName)) {
244                         fcpListenerManager.fireReceivedPersistentPutDir(new PersistentPutDir(fcpMessage));
245                 } else if ("URIGenerated".equals(messageName)) {
246                         fcpListenerManager.fireReceivedURIGenerated(new URIGenerated(fcpMessage));
247                 } else if ("EndListPersistentRequests".equals(messageName)) {
248                         fcpListenerManager.fireReceivedEndListPersistentRequests(new EndListPersistentRequests(fcpMessage));
249                 } else if ("Peer".equals(messageName)) {
250                         fcpListenerManager.fireReceivedPeer(new Peer(fcpMessage));
251                 } else if ("PeerNote".equals(messageName)) {
252                         fcpListenerManager.fireReceivedPeerNote(new PeerNote(fcpMessage));
253                 } else if ("StartedCompression".equals(messageName)) {
254                         fcpListenerManager.fireReceivedStartedCompression(new StartedCompression(fcpMessage));
255                 } else if ("FinishedCompression".equals(messageName)) {
256                         fcpListenerManager.fireReceivedFinishedCompression(new FinishedCompression(fcpMessage));
257                 } else if ("GetFailed".equals(messageName)) {
258                         fcpListenerManager.fireReceivedGetFailed(new GetFailed(fcpMessage));
259                 } else if ("PutFetchable".equals(messageName)) {
260                         fcpListenerManager.fireReceivedPutFetchable(new PutFetchable(fcpMessage));
261                 } else if ("PutSuccessful".equals(messageName)) {
262                         fcpListenerManager.fireReceivedPutSuccessful(new PutSuccessful(fcpMessage));
263                 } else if ("PutFailed".equals(messageName)) {
264                         fcpListenerManager.fireReceivedPutFailed(new PutFailed(fcpMessage));
265                 } else if ("DataFound".equals(messageName)) {
266                         fcpListenerManager.fireReceivedDataFound(new DataFound(fcpMessage));
267                 } else if ("SubscribedUSKUpdate".equals(messageName)) {
268                         fcpListenerManager.fireReceivedSubscribedUSKUpdate(new SubscribedUSKUpdate(fcpMessage));
269                 } else if ("SubscribedUSK".equals(messageName)) {
270                         fcpListenerManager.fireReceivedSubscribedUSK(new SubscribedUSK(fcpMessage));
271                 } else if ("IdentifierCollision".equals(messageName)) {
272                         fcpListenerManager.fireReceivedIdentifierCollision(new IdentifierCollision(fcpMessage));
273                 } else if ("AllData".equals(messageName)) {
274                         InputStream payloadInputStream = getInputStream(FcpUtils.safeParseLong(fcpMessage.getField("DataLength")));
275                         fcpListenerManager.fireReceivedAllData(new AllData(fcpMessage, payloadInputStream));
276                 } else if ("EndListPeerNotes".equals(messageName)) {
277                         fcpListenerManager.fireReceivedEndListPeerNotes(new EndListPeerNotes(fcpMessage));
278                 } else if ("EndListPeers".equals(messageName)) {
279                         fcpListenerManager.fireReceivedEndListPeers(new EndListPeers(fcpMessage));
280                 } else if ("SSKKeypair".equals(messageName)) {
281                         fcpListenerManager.fireReceivedSSKKeypair(new SSKKeypair(fcpMessage));
282                 } else if ("PeerRemoved".equals(messageName)) {
283                         fcpListenerManager.fireReceivedPeerRemoved(new PeerRemoved(fcpMessage));
284                 } else if ("PersistentRequestModified".equals(messageName)) {
285                         fcpListenerManager.fireReceivedPersistentRequestModified(new PersistentRequestModified(fcpMessage));
286                 } else if ("PersistentRequestRemoved".equals(messageName)) {
287                         fcpListenerManager.fireReceivedPersistentRequestRemoved(new PersistentRequestRemoved(fcpMessage));
288                 } else if ("UnknownPeerNoteType".equals(messageName)) {
289                         fcpListenerManager.fireReceivedUnknownPeerNoteType(new UnknownPeerNoteType(fcpMessage));
290                 } else if ("UnknownNodeIdentifier".equals(messageName)) {
291                         fcpListenerManager.fireReceivedUnknownNodeIdentifier(new UnknownNodeIdentifier(fcpMessage));
292                 } else if ("FCPPluginReply".equals(messageName)) {
293                         InputStream payloadInputStream = getInputStream(FcpUtils.safeParseLong(fcpMessage.getField("DataLength")));
294                         fcpListenerManager.fireReceivedFCPPluginReply(new FCPPluginReply(fcpMessage, payloadInputStream));
295                 } else if ("PluginInfo".equals(messageName)) {
296                         fcpListenerManager.fireReceivedPluginInfo(new PluginInfo(fcpMessage));
297                 } else if ("PluginRemoved".equals(messageName)) {
298                         fcpListenerManager.fireReceivedPluginRemoved(new PluginRemoved(fcpMessage));
299                 } else if ("NodeData".equals(messageName)) {
300                         fcpListenerManager.fireReceivedNodeData(new NodeData(fcpMessage));
301                 } else if ("TestDDAReply".equals(messageName)) {
302                         fcpListenerManager.fireReceivedTestDDAReply(new TestDDAReply(fcpMessage));
303                 } else if ("TestDDAComplete".equals(messageName)) {
304                         fcpListenerManager.fireReceivedTestDDAComplete(new TestDDAComplete(fcpMessage));
305                 } else if ("ConfigData".equals(messageName)) {
306                         fcpListenerManager.fireReceivedConfigData(new ConfigData(fcpMessage));
307                 } else if ("NodeHello".equals(messageName)) {
308                         fcpListenerManager.fireReceivedNodeHello(new NodeHello(fcpMessage));
309                 } else if ("CloseConnectionDuplicateClientName".equals(messageName)) {
310                         fcpListenerManager.fireReceivedCloseConnectionDuplicateClientName(new CloseConnectionDuplicateClientName(fcpMessage));
311                 } else if ("SentFeed".equals(messageName)) {
312                         fcpListenerManager.fireSentFeed(new SentFeed(fcpMessage));
313                 } else if ("ReceivedBookmarkFeed".equals(messageName)) {
314                         fcpListenerManager.fireReceivedBookmarkFeed(new ReceivedBookmarkFeed(fcpMessage));
315                 } else {
316                         fcpListenerManager.fireMessageReceived(fcpMessage);
317                 }
318         }
319
320         /**
321          * Handles a disconnect from the node.
322          *
323          * @param throwable
324          *            The exception that caused the disconnect, or
325          *            <code>null</code> if there was no exception
326          */
327         synchronized void handleDisconnect(Throwable throwable) {
328                 FcpUtils.close(remoteInputStream);
329                 FcpUtils.close(remoteOutputStream);
330                 FcpUtils.close(remoteSocket);
331                 if (connectionHandler != null) {
332                         connectionHandler.stop();
333                         connectionHandler = null;
334                         fcpListenerManager.fireConnectionClosed(throwable);
335                 }
336         }
337
338         //
339         // PRIVATE METHODS
340         //
341
342         /**
343          * Incremets the counter in {@link #incomingMessageStatistics} by
344          * <cod>1</code> for the given message name.
345          *
346          * @param name
347          *            The name of the message to count
348          */
349         private void countMessage(String name) {
350                 int oldValue = 0;
351                 if (incomingMessageStatistics.containsKey(name)) {
352                         oldValue = incomingMessageStatistics.get(name);
353                 }
354                 incomingMessageStatistics.put(name, oldValue + 1);
355                 logger.finest("count for " + name + ": " + (oldValue + 1));
356         }
357
358         private synchronized InputStream getInputStream(long dataLength) throws IOException {
359                 return new TempInputStream(remoteInputStream, dataLength);
360         }
361
362 }