2 * jFCPlib - FpcConnection.java - Copyright © 2008 David Roden
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.
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.
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.
19 package net.pterodactylus.fcp;
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;
32 import java.util.logging.Logger;
35 * An FCP connection to a Freenet node.
37 * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
39 public class FcpConnection implements Closeable {
42 private static final Logger logger = Logger.getLogger(FcpConnection.class.getName());
44 /** The default port for FCP v2. */
45 public static final int DEFAULT_PORT = 9481;
47 /** Listener management. */
48 private final FcpListenerManager fcpListenerManager = new FcpListenerManager(this);
50 /** The address of the node. */
51 private final InetAddress address;
53 /** The port number of the node’s FCP port. */
54 private final int port;
56 /** The remote socket. */
57 private Socket remoteSocket;
59 /** The input stream from the node. */
60 private InputStream remoteInputStream;
62 /** The output stream to the node. */
63 private OutputStream remoteOutputStream;
65 /** The connection handler. */
66 private FcpConnectionHandler connectionHandler;
68 /** Incoming message statistics. */
69 private Map<String, Integer> incomingMessageStatistics = Collections.synchronizedMap(new HashMap<String, Integer>());
72 * Creates a new FCP connection to the freenet node running on localhost,
73 * using the default port.
75 * @throws UnknownHostException
76 * if the hostname can not be resolved
78 public FcpConnection() throws UnknownHostException {
79 this(InetAddress.getLocalHost());
83 * Creates a new FCP connection to the Freenet node running on the given
84 * host, listening on the default port.
87 * The hostname of the Freenet node
88 * @throws UnknownHostException
89 * if <code>host</code> can not be resolved
91 public FcpConnection(String host) throws UnknownHostException {
92 this(host, DEFAULT_PORT);
96 * Creates a new FCP connection to the Freenet node running on the given
97 * host, listening on the given port.
100 * The hostname of the Freenet node
102 * The port number of the node’s FCP port
103 * @throws UnknownHostException
104 * if <code>host</code> can not be resolved
106 public FcpConnection(String host, int port) throws UnknownHostException {
107 this(InetAddress.getByName(host), port);
111 * Creates a new FCP connection to the Freenet node running at the given
112 * address, listening on the default port.
115 * The address of the Freenet node
117 public FcpConnection(InetAddress address) {
118 this(address, DEFAULT_PORT);
122 * Creates a new FCP connection to the Freenet node running at the given
123 * address, listening on the given port.
126 * The address of the Freenet node
128 * The port number of the node’s FCP port
130 public FcpConnection(InetAddress address, int port) {
131 this.address = address;
136 // LISTENER MANAGEMENT
140 * Adds the given listener to the list of listeners.
143 * The listener to add
145 public void addFcpListener(FcpListener fcpListener) {
146 fcpListenerManager.addListener(fcpListener);
150 * Removes the given listener from the list of listeners.
153 * The listener to remove
155 public void removeFcpListener(FcpListener fcpListener) {
156 fcpListenerManager.removeListener(fcpListener);
164 * Connects to the node.
166 * @throws IOException
167 * if an I/O error occurs
168 * @throws IllegalStateException
169 * if there is already a connection to the node
171 public synchronized void connect() throws IOException, IllegalStateException {
172 if (connectionHandler != null) {
173 throw new IllegalStateException("already connected, disconnect first");
175 logger.info("connecting to " + address + ":" + port + "…");
176 remoteSocket = new Socket(address, port);
177 remoteInputStream = remoteSocket.getInputStream();
178 remoteOutputStream = remoteSocket.getOutputStream();
179 new Thread(connectionHandler = new FcpConnectionHandler(this, remoteInputStream)).start();
183 * Disconnects from the node. If there is no connection to the node, this
184 * method does nothing.
186 * @deprecated Use {@link #close()} instead
189 public synchronized void disconnect() {
194 * Closes the connection. If there is no connection to the node, this method
197 public void close() {
198 handleDisconnect(null);
202 * Sends the given FCP message.
205 * The FCP message to send
206 * @throws IOException
207 * if an I/O error occurs
209 public synchronized void sendMessage(FcpMessage fcpMessage) throws IOException {
210 logger.fine("sending message: " + fcpMessage.getName());
211 fcpMessage.write(remoteOutputStream);
215 // PACKAGE-PRIVATE METHODS
219 * Handles the given message, notifying listeners. This message should only
220 * be called by {@link FcpConnectionHandler}.
223 * The received message
225 void handleMessage(FcpMessage fcpMessage) {
226 logger.fine("received message: " + fcpMessage.getName());
227 String messageName = fcpMessage.getName();
228 countMessage(messageName);
229 if ("SimpleProgress".equals(messageName)) {
230 fcpListenerManager.fireReceivedSimpleProgress(new SimpleProgress(fcpMessage));
231 } else if ("ProtocolError".equals(messageName)) {
232 fcpListenerManager.fireReceivedProtocolError(new ProtocolError(fcpMessage));
233 } else if ("PersistentGet".equals(messageName)) {
234 fcpListenerManager.fireReceivedPersistentGet(new PersistentGet(fcpMessage));
235 } else if ("PersistentPut".equals(messageName)) {
236 fcpListenerManager.fireReceivedPersistentPut(new PersistentPut(fcpMessage));
237 } else if ("PersistentPutDir".equals(messageName)) {
238 fcpListenerManager.fireReceivedPersistentPutDir(new PersistentPutDir(fcpMessage));
239 } else if ("URIGenerated".equals(messageName)) {
240 fcpListenerManager.fireReceivedURIGenerated(new URIGenerated(fcpMessage));
241 } else if ("EndListPersistentRequests".equals(messageName)) {
242 fcpListenerManager.fireReceivedEndListPersistentRequests(new EndListPersistentRequests(fcpMessage));
243 } else if ("Peer".equals(messageName)) {
244 fcpListenerManager.fireReceivedPeer(new Peer(fcpMessage));
245 } else if ("PeerNote".equals(messageName)) {
246 fcpListenerManager.fireReceivedPeerNote(new PeerNote(fcpMessage));
247 } else if ("StartedCompression".equals(messageName)) {
248 fcpListenerManager.fireReceivedStartedCompression(new StartedCompression(fcpMessage));
249 } else if ("FinishedCompression".equals(messageName)) {
250 fcpListenerManager.fireReceivedFinishedCompression(new FinishedCompression(fcpMessage));
251 } else if ("GetFailed".equals(messageName)) {
252 fcpListenerManager.fireReceivedGetFailed(new GetFailed(fcpMessage));
253 } else if ("PutFetchable".equals(messageName)) {
254 fcpListenerManager.fireReceivedPutFetchable(new PutFetchable(fcpMessage));
255 } else if ("PutSuccessful".equals(messageName)) {
256 fcpListenerManager.fireReceivedPutSuccessful(new PutSuccessful(fcpMessage));
257 } else if ("PutFailed".equals(messageName)) {
258 fcpListenerManager.fireReceivedPutFailed(new PutFailed(fcpMessage));
259 } else if ("DataFound".equals(messageName)) {
260 fcpListenerManager.fireReceivedDataFound(new DataFound(fcpMessage));
261 } else if ("SubscribedUSKUpdate".equals(messageName)) {
262 fcpListenerManager.fireReceivedSubscribedUSKUpdate(new SubscribedUSKUpdate(fcpMessage));
263 } else if ("IdentifierCollision".equals(messageName)) {
264 fcpListenerManager.fireReceivedIdentifierCollision(new IdentifierCollision(fcpMessage));
265 } else if ("AllData".equals(messageName)) {
266 LimitedInputStream payloadInputStream = getInputStream(FcpUtils.safeParseLong(fcpMessage.getField("DataLength")));
267 fcpListenerManager.fireReceivedAllData(new AllData(fcpMessage, payloadInputStream));
269 payloadInputStream.consume();
270 } catch (IOException ioe1) {
271 /* well, ignore. when the connection handler fails, all fails. */
273 } else if ("EndListPeerNotes".equals(messageName)) {
274 fcpListenerManager.fireReceivedEndListPeerNotes(new EndListPeerNotes(fcpMessage));
275 } else if ("EndListPeers".equals(messageName)) {
276 fcpListenerManager.fireReceivedEndListPeers(new EndListPeers(fcpMessage));
277 } else if ("SSKKeypair".equals(messageName)) {
278 fcpListenerManager.fireReceivedSSKKeypair(new SSKKeypair(fcpMessage));
279 } else if ("PeerRemoved".equals(messageName)) {
280 fcpListenerManager.fireReceivedPeerRemoved(new PeerRemoved(fcpMessage));
281 } else if ("PersistentRequestModified".equals(messageName)) {
282 fcpListenerManager.fireReceivedPersistentRequestModified(new PersistentRequestModified(fcpMessage));
283 } else if ("PersistentRequestRemoved".equals(messageName)) {
284 fcpListenerManager.fireReceivedPersistentRequestRemoved(new PersistentRequestRemoved(fcpMessage));
285 } else if ("UnknownPeerNoteType".equals(messageName)) {
286 fcpListenerManager.fireReceivedUnknownPeerNoteType(new UnknownPeerNoteType(fcpMessage));
287 } else if ("UnknownNodeIdentifier".equals(messageName)) {
288 fcpListenerManager.fireReceivedUnknownNodeIdentifier(new UnknownNodeIdentifier(fcpMessage));
289 } else if ("FCPPluginReply".equals(messageName)) {
290 LimitedInputStream payloadInputStream = getInputStream(FcpUtils.safeParseLong(fcpMessage.getField("DataLength")));
291 fcpListenerManager.fireReceivedFCPPluginReply(new FCPPluginReply(fcpMessage, payloadInputStream));
293 payloadInputStream.consume();
294 } catch (IOException ioe1) {
297 } else if ("PluginInfo".equals(messageName)) {
298 fcpListenerManager.fireReceivedPluginInfo(new PluginInfo(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 ("ReceivedBookmarkFeed".equals(messageName)) {
312 fcpListenerManager.fireReceivedBookmarkFeed(new ReceivedBookmarkFeed(fcpMessage));
314 fcpListenerManager.fireMessageReceived(fcpMessage);
319 * Handles a disconnect from the node.
322 * The exception that caused the disconnect, or <code>null</code>
323 * if there was no exception
325 synchronized void handleDisconnect(Throwable throwable) {
326 FcpUtils.close(remoteInputStream);
327 FcpUtils.close(remoteOutputStream);
328 FcpUtils.close(remoteSocket);
329 if (connectionHandler != null) {
330 connectionHandler.stop();
331 connectionHandler = null;
332 fcpListenerManager.fireConnectionClosed(throwable);
341 * Incremets the counter in {@link #incomingMessageStatistics} by
342 * <cod>1</code> for the given message name.
345 * The name of the message to count
347 private void countMessage(String name) {
349 if (incomingMessageStatistics.containsKey(name)) {
350 oldValue = incomingMessageStatistics.get(name);
352 incomingMessageStatistics.put(name, oldValue + 1);
353 logger.finest("count for " + name + ": " + (oldValue + 1));
357 * Returns a limited input stream from the node’s input stream.
360 * The length of the stream
361 * @return The limited input stream
363 private synchronized LimitedInputStream getInputStream(long dataLength) {
364 if (dataLength <= 0) {
365 return new LimitedInputStream(null, 0);
367 return new LimitedInputStream(remoteInputStream, dataLength);
371 * A wrapper around an {@link InputStream} that only supplies a limit number
372 * of bytes from the underlying input stream.
374 * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
376 private static class LimitedInputStream extends FilterInputStream {
378 /** The remaining number of bytes that can be read. */
379 private long remaining;
382 * Creates a new LimitedInputStream that supplies at most
383 * <code>length</code> bytes from the given input stream.
388 * The number of bytes to read
390 public LimitedInputStream(InputStream inputStream, long length) {
396 * @see java.io.FilterInputStream#available()
399 public synchronized int available() throws IOException {
400 if (remaining == 0) {
403 return (int) Math.min(super.available(), Math.min(Integer.MAX_VALUE, remaining));
407 * @see java.io.FilterInputStream#read()
410 public synchronized int read() throws IOException {
420 * @see java.io.FilterInputStream#read(byte[], int, int)
423 public synchronized int read(byte[] b, int off, int len) throws IOException {
424 if (remaining == 0) {
427 int toCopy = (int) Math.min(len, Math.min(remaining, Integer.MAX_VALUE));
428 int read = super.read(b, off, toCopy);
434 * @see java.io.FilterInputStream#skip(long)
437 public synchronized long skip(long n) throws IOException {
438 if ((n < 0) || (remaining == 0)) {
441 long skipped = super.skip(Math.min(n, remaining));
442 remaining -= skipped;
447 * {@inheritDoc} This method does nothing, as {@link #mark(int)} and
448 * {@link #reset()} are not supported.
450 * @see java.io.FilterInputStream#mark(int)
453 public void mark(int readlimit) {
460 * @see java.io.FilterInputStream#markSupported()
461 * @return <code>false</code>
464 public boolean markSupported() {
469 * {@inheritDoc} This method does nothing, as {@link #mark(int)} and
470 * {@link #reset()} are not supported.
472 * @see java.io.FilterInputStream#reset()
475 public void reset() throws IOException {
480 * Consumes the input stream, i.e. read all bytes until the limit is
483 * @throws IOException
484 * if an I/O error occurs
486 public synchronized void consume() throws IOException {
487 while (remaining > 0) {