Allow detaching the client from its connection.
[jFCPlib.git] / src / main / java / net / pterodactylus / fcp / highlevel / FcpClient.java
1 /*
2  * jFCPlib - FcpClient.java - Copyright © 2009 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.highlevel;
20
21 import java.io.IOException;
22 import java.io.InputStream;
23 import java.net.InetAddress;
24 import java.net.URL;
25 import java.net.UnknownHostException;
26 import java.util.Collection;
27 import java.util.Collections;
28 import java.util.HashMap;
29 import java.util.HashSet;
30 import java.util.Map;
31 import java.util.Set;
32 import java.util.Map.Entry;
33 import java.util.concurrent.CountDownLatch;
34
35 import net.pterodactylus.fcp.AddPeer;
36 import net.pterodactylus.fcp.ClientHello;
37 import net.pterodactylus.fcp.CloseConnectionDuplicateClientName;
38 import net.pterodactylus.fcp.DataFound;
39 import net.pterodactylus.fcp.EndListPeerNotes;
40 import net.pterodactylus.fcp.EndListPeers;
41 import net.pterodactylus.fcp.EndListPersistentRequests;
42 import net.pterodactylus.fcp.FCPPluginMessage;
43 import net.pterodactylus.fcp.FCPPluginReply;
44 import net.pterodactylus.fcp.FcpAdapter;
45 import net.pterodactylus.fcp.FcpConnection;
46 import net.pterodactylus.fcp.FcpListener;
47 import net.pterodactylus.fcp.GenerateSSK;
48 import net.pterodactylus.fcp.GetFailed;
49 import net.pterodactylus.fcp.GetNode;
50 import net.pterodactylus.fcp.ListPeerNotes;
51 import net.pterodactylus.fcp.ListPeers;
52 import net.pterodactylus.fcp.ListPersistentRequests;
53 import net.pterodactylus.fcp.ModifyPeer;
54 import net.pterodactylus.fcp.ModifyPeerNote;
55 import net.pterodactylus.fcp.NodeData;
56 import net.pterodactylus.fcp.NodeHello;
57 import net.pterodactylus.fcp.NodeRef;
58 import net.pterodactylus.fcp.Peer;
59 import net.pterodactylus.fcp.PeerNote;
60 import net.pterodactylus.fcp.PeerRemoved;
61 import net.pterodactylus.fcp.PersistentGet;
62 import net.pterodactylus.fcp.PersistentPut;
63 import net.pterodactylus.fcp.ProtocolError;
64 import net.pterodactylus.fcp.RemovePeer;
65 import net.pterodactylus.fcp.SSKKeypair;
66 import net.pterodactylus.fcp.SimpleProgress;
67 import net.pterodactylus.fcp.WatchGlobal;
68 import net.pterodactylus.util.filter.Filter;
69 import net.pterodactylus.util.filter.Filters;
70 import net.pterodactylus.util.thread.ObjectWrapper;
71
72 /**
73  * High-level FCP client that hides the details of the underlying FCP
74  * implementation.
75  *
76  * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
77  */
78 public class FcpClient {
79
80         /** Object used for synchronization. */
81         private final Object syncObject = new Object();
82
83         /** Listener management. */
84         private final FcpClientListenerManager fcpClientListenerManager = new FcpClientListenerManager(this);
85
86         /** The underlying FCP connection. */
87         private final FcpConnection fcpConnection;
88
89         /** The {@link NodeHello} data sent by the node on connection. */
90         private volatile NodeHello nodeHello;
91
92         /** Whether the client is currently connected. */
93         private volatile boolean connected;
94
95         /** The listener for “connection closed” events. */
96         private FcpListener connectionClosedListener;
97
98         /**
99          * Creates an FCP client with the given name.
100          *
101          * @throws UnknownHostException
102          *             if the hostname “localhost” is unknown
103          */
104         public FcpClient() throws UnknownHostException {
105                 this("localhost");
106         }
107
108         /**
109          * Creates an FCP client.
110          *
111          * @param hostname
112          *            The hostname of the Freenet node
113          * @throws UnknownHostException
114          *             if the given hostname can not be resolved
115          */
116         public FcpClient(String hostname) throws UnknownHostException {
117                 this(hostname, FcpConnection.DEFAULT_PORT);
118         }
119
120         /**
121          * Creates an FCP client.
122          *
123          * @param hostname
124          *            The hostname of the Freenet node
125          * @param port
126          *            The Freenet node’s FCP port
127          * @throws UnknownHostException
128          *             if the given hostname can not be resolved
129          */
130         public FcpClient(String hostname, int port) throws UnknownHostException {
131                 this(InetAddress.getByName(hostname), port);
132         }
133
134         /**
135          * Creates an FCP client.
136          *
137          * @param host
138          *            The host address of the Freenet node
139          */
140         public FcpClient(InetAddress host) {
141                 this(host, FcpConnection.DEFAULT_PORT);
142         }
143
144         /**
145          * Creates an FCP client.
146          *
147          * @param host
148          *            The host address of the Freenet node
149          * @param port
150          *            The Freenet node’s FCP port
151          */
152         public FcpClient(InetAddress host, int port) {
153                 this(new FcpConnection(host, port));
154         }
155
156         /**
157          * Creates a new high-level FCP client that will use the given connection.
158          * This constructor will assume that the FCP connection is already
159          * connected.
160          *
161          * @param fcpConnection
162          *            The FCP connection to use
163          */
164         public FcpClient(FcpConnection fcpConnection) {
165                 this(fcpConnection, true);
166         }
167
168         /**
169          * Creates a new high-level FCP client that will use the given connection.
170          *
171          * @param fcpConnection
172          *            The FCP connection to use
173          * @param connected
174          *            The initial status of the FCP connection
175          */
176         public FcpClient(FcpConnection fcpConnection, boolean connected) {
177                 this.fcpConnection = fcpConnection;
178                 this.connected = connected;
179                 connectionClosedListener = new FcpAdapter() {
180
181                         /**
182                          * {@inheritDoc}
183                          */
184                         @Override
185                         @SuppressWarnings("synthetic-access")
186                         public void connectionClosed(FcpConnection fcpConnection, Throwable throwable) {
187                                 FcpClient.this.connected = false;
188                                 fcpClientListenerManager.fireFcpClientDisconnected();
189                         }
190                 };
191                 fcpConnection.addFcpListener(connectionClosedListener);
192         }
193
194         //
195         // LISTENER MANAGEMENT
196         //
197
198         /**
199          * Adds an FCP listener to the underlying connection.
200          *
201          * @param fcpListener
202          *            The FCP listener to add
203          */
204         public void addFcpListener(FcpListener fcpListener) {
205                 fcpConnection.addFcpListener(fcpListener);
206         }
207
208         /**
209          * Removes an FCP listener from the underlying connection.
210          *
211          * @param fcpListener
212          *            The FCP listener to remove
213          */
214         public void removeFcpListener(FcpListener fcpListener) {
215                 fcpConnection.removeFcpListener(fcpListener);
216         }
217
218         /**
219          * Adds an FCP client listener to the list of registered listeners.
220          *
221          * @param fcpClientListener
222          *            The FCP client listener to add
223          */
224         public void addFcpClientListener(FcpClientListener fcpClientListener) {
225                 fcpClientListenerManager.addListener(fcpClientListener);
226         }
227
228         /**
229          * Removes an FCP client listener from the list of registered listeners.
230          *
231          * @param fcpClientListener
232          *            The FCP client listener to remove
233          */
234         public void removeFcpClientListener(FcpClientListener fcpClientListener) {
235                 fcpClientListenerManager.removeListener(fcpClientListener);
236         }
237
238         //
239         // ACCESSORS
240         //
241
242         /**
243          * Returns the {@link NodeHello} object that the node returned when
244          * connecting.
245          *
246          * @return The {@code NodeHello} data container
247          */
248         public NodeHello getNodeHello() {
249                 return nodeHello;
250         }
251
252         /**
253          * Returns the underlying FCP connection.
254          *
255          * @return The underlying FCP connection
256          */
257         public FcpConnection getConnection() {
258                 return fcpConnection;
259         }
260
261         //
262         // ACTIONS
263         //
264
265         /**
266          * Connects the FCP client.
267          *
268          * @param name
269          *            The name of the client
270          * @throws IOException
271          *             if an I/O error occurs
272          * @throws FcpException
273          *             if an FCP error occurs
274          */
275         public void connect(final String name) throws IOException, FcpException {
276                 checkConnected(false);
277                 connected = true;
278                 new ExtendedFcpAdapter() {
279
280                         /**
281                          * {@inheritDoc}
282                          */
283                         @Override
284                         @SuppressWarnings("synthetic-access")
285                         public void run() throws IOException {
286                                 fcpConnection.connect();
287                                 ClientHello clientHello = new ClientHello(name);
288                                 fcpConnection.sendMessage(clientHello);
289                                 WatchGlobal watchGlobal = new WatchGlobal(true);
290                                 fcpConnection.sendMessage(watchGlobal);
291                         }
292
293                         /**
294                          * {@inheritDoc}
295                          */
296                         @Override
297                         @SuppressWarnings("synthetic-access")
298                         public void receivedNodeHello(FcpConnection fcpConnection, NodeHello nodeHello) {
299                                 FcpClient.this.nodeHello = nodeHello;
300                                 completionLatch.countDown();
301                         }
302                 }.execute();
303         }
304
305         /**
306          * Disconnects the FCP client.
307          */
308         public void disconnect() {
309                 synchronized (syncObject) {
310                         fcpConnection.close();
311                         syncObject.notifyAll();
312                 }
313         }
314
315         /**
316          * Returns whether this client is currently connected.
317          *
318          * @return {@code true} if the client is currently connected, {@code false}
319          *         otherwise
320          */
321         public boolean isConnected() {
322                 return connected;
323         }
324
325         /**
326          * Detaches this client from its underlying FCP connection.
327          */
328         public void detach() {
329                 fcpConnection.removeFcpListener(connectionClosedListener);
330         }
331
332         //
333         // PEER MANAGEMENT
334         //
335
336         /**
337          * Returns all peers that the node has.
338          *
339          * @param withMetadata
340          *            <code>true</code> to include peer metadata
341          * @param withVolatile
342          *            <code>true</code> to include volatile peer data
343          * @return A set containing the node’s peers
344          * @throws IOException
345          *             if an I/O error occurs
346          * @throws FcpException
347          *             if an FCP error occurs
348          */
349         public Collection<Peer> getPeers(final boolean withMetadata, final boolean withVolatile) throws IOException, FcpException {
350                 final Set<Peer> peers = Collections.synchronizedSet(new HashSet<Peer>());
351                 new ExtendedFcpAdapter() {
352
353                         /** The ID of the “ListPeers” request. */
354                         @SuppressWarnings("synthetic-access")
355                         private String identifier = createIdentifier("list-peers");
356
357                         /**
358                          * {@inheritDoc}
359                          */
360                         @Override
361                         @SuppressWarnings("synthetic-access")
362                         public void run() throws IOException {
363                                 fcpConnection.sendMessage(new ListPeers(identifier, withMetadata, withVolatile));
364                         }
365
366                         /**
367                          * {@inheritDoc}
368                          */
369                         @Override
370                         public void receivedPeer(FcpConnection fcpConnection, Peer peer) {
371                                 if (peer.getIdentifier().equals(identifier)) {
372                                         peers.add(peer);
373                                 }
374                         }
375
376                         /**
377                          * {@inheritDoc}
378                          */
379                         @Override
380                         public void receivedEndListPeers(FcpConnection fcpConnection, EndListPeers endListPeers) {
381                                 if (endListPeers.getIdentifier().equals(identifier)) {
382                                         completionLatch.countDown();
383                                 }
384                         }
385                 }.execute();
386                 return peers;
387         }
388
389         /**
390          * Returns all darknet peers.
391          *
392          * @param withMetadata
393          *            <code>true</code> to include peer metadata
394          * @param withVolatile
395          *            <code>true</code> to include volatile peer data
396          * @return A set containing the node’s darknet peers
397          * @throws IOException
398          *             if an I/O error occurs
399          * @throws FcpException
400          *             if an FCP error occurs
401          */
402         public Collection<Peer> getDarknetPeers(boolean withMetadata, boolean withVolatile) throws IOException, FcpException {
403                 Collection<Peer> allPeers = getPeers(withMetadata, withVolatile);
404                 Collection<Peer> darknetPeers = new HashSet<Peer>();
405                 for (Peer peer : allPeers) {
406                         if (!peer.isOpennet() && !peer.isSeed()) {
407                                 darknetPeers.add(peer);
408                         }
409                 }
410                 return darknetPeers;
411         }
412
413         /**
414          * Returns all opennet peers.
415          *
416          * @param withMetadata
417          *            <code>true</code> to include peer metadata
418          * @param withVolatile
419          *            <code>true</code> to include volatile peer data
420          * @return A set containing the node’s opennet peers
421          * @throws IOException
422          *             if an I/O error occurs
423          * @throws FcpException
424          *             if an FCP error occurs
425          */
426         public Collection<Peer> getOpennetPeers(boolean withMetadata, boolean withVolatile) throws IOException, FcpException {
427                 Collection<Peer> allPeers = getPeers(withMetadata, withVolatile);
428                 Collection<Peer> opennetPeers = new HashSet<Peer>();
429                 for (Peer peer : allPeers) {
430                         if (peer.isOpennet() && !peer.isSeed()) {
431                                 opennetPeers.add(peer);
432                         }
433                 }
434                 return opennetPeers;
435         }
436
437         /**
438          * Returns all seed peers.
439          *
440          * @param withMetadata
441          *            <code>true</code> to include peer metadata
442          * @param withVolatile
443          *            <code>true</code> to include volatile peer data
444          * @return A set containing the node’s seed peers
445          * @throws IOException
446          *             if an I/O error occurs
447          * @throws FcpException
448          *             if an FCP error occurs
449          */
450         public Collection<Peer> getSeedPeers(boolean withMetadata, boolean withVolatile) throws IOException, FcpException {
451                 Collection<Peer> allPeers = getPeers(withMetadata, withVolatile);
452                 Collection<Peer> seedPeers = new HashSet<Peer>();
453                 for (Peer peer : allPeers) {
454                         if (peer.isSeed()) {
455                                 seedPeers.add(peer);
456                         }
457                 }
458                 return seedPeers;
459         }
460
461         /**
462          * Adds the given peer to the node.
463          *
464          * @param peer
465          *            The peer to add
466          * @throws IOException
467          *             if an I/O error occurs
468          * @throws FcpException
469          *             if an FCP error occurs
470          */
471         public void addPeer(Peer peer) throws IOException, FcpException {
472                 addPeer(peer.getNodeRef());
473         }
474
475         /**
476          * Adds the peer defined by the noderef to the node.
477          *
478          * @param nodeRef
479          *            The noderef that defines the new peer
480          * @throws IOException
481          *             if an I/O error occurs
482          * @throws FcpException
483          *             if an FCP error occurs
484          */
485         public void addPeer(NodeRef nodeRef) throws IOException, FcpException {
486                 addPeer(new AddPeer(nodeRef));
487         }
488
489         /**
490          * Adds a peer, reading the noderef from the given URL.
491          *
492          * @param url
493          *            The URL to read the noderef from
494          * @throws IOException
495          *             if an I/O error occurs
496          * @throws FcpException
497          *             if an FCP error occurs
498          */
499         public void addPeer(URL url) throws IOException, FcpException {
500                 addPeer(new AddPeer(url));
501         }
502
503         /**
504          * Adds a peer, reading the noderef of the peer from the given file.
505          * <strong>Note:</strong> the file to read the noderef from has to reside on
506          * the same machine as the node!
507          *
508          * @param file
509          *            The name of the file containing the peer’s noderef
510          * @throws IOException
511          *             if an I/O error occurs
512          * @throws FcpException
513          *             if an FCP error occurs
514          */
515         public void addPeer(String file) throws IOException, FcpException {
516                 addPeer(new AddPeer(file));
517         }
518
519         /**
520          * Sends the given {@link AddPeer} message to the node. This method should
521          * not be called directly. Use one of {@link #addPeer(Peer)},
522          * {@link #addPeer(NodeRef)}, {@link #addPeer(URL)}, or
523          * {@link #addPeer(String)} instead.
524          *
525          * @param addPeer
526          *            The “AddPeer” message
527          * @throws IOException
528          *             if an I/O error occurs
529          * @throws FcpException
530          *             if an FCP error occurs
531          */
532         private void addPeer(final AddPeer addPeer) throws IOException, FcpException {
533                 new ExtendedFcpAdapter() {
534
535                         /**
536                          * {@inheritDoc}
537                          */
538                         @Override
539                         @SuppressWarnings("synthetic-access")
540                         public void run() throws IOException {
541                                 fcpConnection.sendMessage(addPeer);
542                         }
543
544                         /**
545                          * {@inheritDoc}
546                          */
547                         @Override
548                         public void receivedPeer(FcpConnection fcpConnection, Peer peer) {
549                                 completionLatch.countDown();
550                         }
551                 }.execute();
552         }
553
554         /**
555          * Modifies the given peer.
556          *
557          * @param peer
558          *            The peer to modify
559          * @param allowLocalAddresses
560          *            <code>true</code> to allow local address, <code>false</code>
561          *            to not allow local address, <code>null</code> to not change
562          *            the setting
563          * @param disabled
564          *            <code>true</code> to disable the peer, <code>false</code> to
565          *            enable the peer, <code>null</code> to not change the setting
566          * @param listenOnly
567          *            <code>true</code> to enable “listen only” for the peer,
568          *            <code>false</code> to disable it, <code>null</code> to not
569          *            change it
570          * @throws IOException
571          *             if an I/O error occurs
572          * @throws FcpException
573          *             if an FCP error occurs
574          */
575         public void modifyPeer(final Peer peer, final Boolean allowLocalAddresses, final Boolean disabled, final Boolean listenOnly) throws IOException, FcpException {
576                 new ExtendedFcpAdapter() {
577
578                         /**
579                          * {@inheritDoc}
580                          */
581                         @Override
582                         @SuppressWarnings("synthetic-access")
583                         public void run() throws IOException {
584                                 fcpConnection.sendMessage(new ModifyPeer(peer.getIdentity(), allowLocalAddresses, disabled, listenOnly));
585                         }
586
587                         /**
588                          * {@inheritDoc}
589                          */
590                         @Override
591                         public void receivedPeer(FcpConnection fcpConnection, Peer peer) {
592                                 completionLatch.countDown();
593                         }
594                 }.execute();
595         }
596
597         /**
598          * Removes the given peer.
599          *
600          * @param peer
601          *            The peer to remove
602          * @throws IOException
603          *             if an I/O error occurs
604          * @throws FcpException
605          *             if an FCP error occurs
606          */
607         public void removePeer(final Peer peer) throws IOException, FcpException {
608                 new ExtendedFcpAdapter() {
609
610                         /**
611                          * {@inheritDoc}
612                          */
613                         @Override
614                         @SuppressWarnings("synthetic-access")
615                         public void run() throws IOException {
616                                 fcpConnection.sendMessage(new RemovePeer(peer.getIdentity()));
617                         }
618
619                         /**
620                          * {@inheritDoc}
621                          */
622                         @Override
623                         public void receivedPeerRemoved(FcpConnection fcpConnection, PeerRemoved peerRemoved) {
624                                 completionLatch.countDown();
625                         }
626                 }.execute();
627         }
628
629         //
630         // PEER NOTES MANAGEMENT
631         //
632
633         /**
634          * Returns the peer note of the given peer.
635          *
636          * @param peer
637          *            The peer to get the note for
638          * @return The peer’s note
639          * @throws IOException
640          *             if an I/O error occurs
641          * @throws FcpException
642          *             if an FCP error occurs
643          */
644         public PeerNote getPeerNote(final Peer peer) throws IOException, FcpException {
645                 final ObjectWrapper<PeerNote> objectWrapper = new ObjectWrapper<PeerNote>();
646                 new ExtendedFcpAdapter() {
647
648                         /**
649                          * {@inheritDoc}
650                          */
651                         @Override
652                         @SuppressWarnings("synthetic-access")
653                         public void run() throws IOException {
654                                 fcpConnection.sendMessage(new ListPeerNotes(peer.getIdentity()));
655                         }
656
657                         /**
658                          * {@inheritDoc}
659                          */
660                         @Override
661                         public void receivedPeerNote(FcpConnection fcpConnection, PeerNote peerNote) {
662                                 if (peerNote.getNodeIdentifier().equals(peer.getIdentity())) {
663                                         objectWrapper.set(peerNote);
664                                 }
665                         }
666
667                         /**
668                          * {@inheritDoc}
669                          */
670                         @Override
671                         public void receivedEndListPeerNotes(FcpConnection fcpConnection, EndListPeerNotes endListPeerNotes) {
672                                 completionLatch.countDown();
673                         }
674                 }.execute();
675                 return objectWrapper.get();
676         }
677
678         /**
679          * Replaces the peer note for the given peer.
680          *
681          * @param peer
682          *            The peer
683          * @param noteText
684          *            The new base64-encoded note text
685          * @param noteType
686          *            The type of the note (currently only <code>1</code> is
687          *            allowed)
688          * @throws IOException
689          *             if an I/O error occurs
690          * @throws FcpException
691          *             if an FCP error occurs
692          */
693         public void modifyPeerNote(final Peer peer, final String noteText, final int noteType) throws IOException, FcpException {
694                 new ExtendedFcpAdapter() {
695
696                         /**
697                          * {@inheritDoc}
698                          */
699                         @Override
700                         @SuppressWarnings("synthetic-access")
701                         public void run() throws IOException {
702                                 fcpConnection.sendMessage(new ModifyPeerNote(peer.getIdentity(), noteText, noteType));
703                         }
704
705                         /**
706                          * {@inheritDoc}
707                          */
708                         @Override
709                         public void receivedPeer(FcpConnection fcpConnection, Peer receivedPeer) {
710                                 if (receivedPeer.getIdentity().equals(peer.getIdentity())) {
711                                         completionLatch.countDown();
712                                 }
713                         }
714                 }.execute();
715         }
716
717         //
718         // KEY GENERATION
719         //
720
721         /**
722          * Generates a new SSK key pair.
723          *
724          * @return The generated key pair
725          * @throws IOException
726          *             if an I/O error occurs
727          * @throws FcpException
728          *             if an FCP error occurs
729          */
730         public SSKKeypair generateKeyPair() throws IOException, FcpException {
731                 final ObjectWrapper<SSKKeypair> sskKeypairWrapper = new ObjectWrapper<SSKKeypair>();
732                 new ExtendedFcpAdapter() {
733
734                         /**
735                          * {@inheritDoc}
736                          */
737                         @Override
738                         @SuppressWarnings("synthetic-access")
739                         public void run() throws IOException {
740                                 fcpConnection.sendMessage(new GenerateSSK());
741                         }
742
743                         /**
744                          * {@inheritDoc}
745                          */
746                         @Override
747                         public void receivedSSKKeypair(FcpConnection fcpConnection, SSKKeypair sskKeypair) {
748                                 sskKeypairWrapper.set(sskKeypair);
749                                 completionLatch.countDown();
750                         }
751                 }.execute();
752                 return sskKeypairWrapper.get();
753         }
754
755         //
756         // REQUEST MANAGEMENT
757         //
758
759         /**
760          * Returns all currently visible persistent get requests.
761          *
762          * @param global
763          *            <code>true</code> to return get requests from the global
764          *            queue, <code>false</code> to only show requests from the
765          *            client-local queue
766          * @return All get requests
767          * @throws IOException
768          *             if an I/O error occurs
769          * @throws FcpException
770          *             if an FCP error occurs
771          */
772         public Collection<Request> getGetRequests(final boolean global) throws IOException, FcpException {
773                 return Filters.filteredCollection(getRequests(global), new Filter<Request>() {
774
775                         /**
776                          * {@inheritDoc}
777                          */
778                         public boolean filterObject(Request request) {
779                                 return request instanceof GetRequest;
780                         }
781                 });
782         }
783
784         /**
785          * Returns all currently visible persistent put requests.
786          *
787          * @param global
788          *            <code>true</code> to return put requests from the global
789          *            queue, <code>false</code> to only show requests from the
790          *            client-local queue
791          * @return All put requests
792          * @throws IOException
793          *             if an I/O error occurs
794          * @throws FcpException
795          *             if an FCP error occurs
796          */
797         public Collection<Request> getPutRequests(final boolean global) throws IOException, FcpException {
798                 return Filters.filteredCollection(getRequests(global), new Filter<Request>() {
799
800                         /**
801                          * {@inheritDoc}
802                          */
803                         public boolean filterObject(Request request) {
804                                 return request instanceof PutRequest;
805                         }
806                 });
807         }
808
809         /**
810          * Returns all currently visible persistent requests.
811          *
812          * @param global
813          *            <code>true</code> to return requests from the global queue,
814          *            <code>false</code> to only show requests from the client-local
815          *            queue
816          * @return All requests
817          * @throws IOException
818          *             if an I/O error occurs
819          * @throws FcpException
820          *             if an FCP error occurs
821          */
822         public Collection<Request> getRequests(final boolean global) throws IOException, FcpException {
823                 final Map<String, Request> requests = Collections.synchronizedMap(new HashMap<String, Request>());
824                 new ExtendedFcpAdapter() {
825
826                         /**
827                          * {@inheritDoc}
828                          */
829                         @Override
830                         @SuppressWarnings("synthetic-access")
831                         public void run() throws IOException {
832                                 fcpConnection.sendMessage(new ListPersistentRequests());
833                         }
834
835                         /**
836                          * {@inheritDoc}
837                          */
838                         @Override
839                         public void receivedPersistentGet(FcpConnection fcpConnection, PersistentGet persistentGet) {
840                                 if (!persistentGet.isGlobal() || global) {
841                                         GetRequest getRequest = new GetRequest(persistentGet);
842                                         requests.put(persistentGet.getIdentifier(), getRequest);
843                                 }
844                         }
845
846                         /**
847                          * {@inheritDoc}
848                          *
849                          * @see net.pterodactylus.fcp.FcpAdapter#receivedDataFound(net.pterodactylus.fcp.FcpConnection,
850                          *      net.pterodactylus.fcp.DataFound)
851                          */
852                         @Override
853                         public void receivedDataFound(FcpConnection fcpConnection, DataFound dataFound) {
854                                 Request getRequest = requests.get(dataFound.getIdentifier());
855                                 if (getRequest == null) {
856                                         return;
857                                 }
858                                 getRequest.setComplete(true);
859                                 getRequest.setLength(dataFound.getDataLength());
860                                 getRequest.setContentType(dataFound.getMetadataContentType());
861                         }
862
863                         /**
864                          * {@inheritDoc}
865                          *
866                          * @see net.pterodactylus.fcp.FcpAdapter#receivedGetFailed(net.pterodactylus.fcp.FcpConnection,
867                          *      net.pterodactylus.fcp.GetFailed)
868                          */
869                         @Override
870                         public void receivedGetFailed(FcpConnection fcpConnection, GetFailed getFailed) {
871                                 Request getRequest = requests.get(getFailed.getIdentifier());
872                                 if (getRequest == null) {
873                                         return;
874                                 }
875                                 getRequest.setComplete(true);
876                                 getRequest.setFailed(true);
877                                 getRequest.setFatal(getFailed.isFatal());
878                                 getRequest.setErrorCode(getFailed.getCode());
879                         }
880
881                         /**
882                          * {@inheritDoc}
883                          *
884                          * @see net.pterodactylus.fcp.FcpAdapter#receivedPersistentPut(net.pterodactylus.fcp.FcpConnection,
885                          *      net.pterodactylus.fcp.PersistentPut)
886                          */
887                         @Override
888                         public void receivedPersistentPut(FcpConnection fcpConnection, PersistentPut persistentPut) {
889                                 if (!persistentPut.isGlobal() || global) {
890                                         PutRequest putRequest = new PutRequest(persistentPut);
891                                         requests.put(persistentPut.getIdentifier(), putRequest);
892                                 }
893                         }
894
895                         /**
896                          * {@inheritDoc}
897                          *
898                          * @see net.pterodactylus.fcp.FcpAdapter#receivedSimpleProgress(net.pterodactylus.fcp.FcpConnection,
899                          *      net.pterodactylus.fcp.SimpleProgress)
900                          */
901                         @Override
902                         public void receivedSimpleProgress(FcpConnection fcpConnection, SimpleProgress simpleProgress) {
903                                 Request request = requests.get(simpleProgress.getIdentifier());
904                                 if (request == null) {
905                                         return;
906                                 }
907                                 request.setTotalBlocks(simpleProgress.getTotal());
908                                 request.setRequiredBlocks(simpleProgress.getRequired());
909                                 request.setFailedBlocks(simpleProgress.getFailed());
910                                 request.setFatallyFailedBlocks(simpleProgress.getFatallyFailed());
911                                 request.setSucceededBlocks(simpleProgress.getSucceeded());
912                                 request.setFinalizedTotal(simpleProgress.isFinalizedTotal());
913                         }
914
915                         /**
916                          * {@inheritDoc}
917                          */
918                         @Override
919                         public void receivedEndListPersistentRequests(FcpConnection fcpConnection, EndListPersistentRequests endListPersistentRequests) {
920                                 completionLatch.countDown();
921                         }
922                 }.execute();
923                 return requests.values();
924         }
925
926         /**
927          * Sends a message to a plugin and waits for the response.
928          *
929          * @param pluginClass
930          *            The name of the plugin class
931          * @param parameters
932          *            The parameters for the plugin
933          * @return The responses from the plugin
934          * @throws FcpException
935          *             if an FCP error occurs
936          * @throws IOException
937          *             if an I/O error occurs
938          */
939         public Map<String, String> sendPluginMessage(String pluginClass, Map<String, String> parameters) throws IOException, FcpException {
940                 return sendPluginMessage(pluginClass, parameters, 0, null);
941         }
942
943         /**
944          * Sends a message to a plugin and waits for the response.
945          *
946          * @param pluginClass
947          *            The name of the plugin class
948          * @param parameters
949          *            The parameters for the plugin
950          * @param dataLength
951          *            The length of the optional data stream, or {@code 0} if there
952          *            is no optional data stream
953          * @param dataInputStream
954          *            The input stream for the payload, or {@code null} if there is
955          *            no payload
956          * @return The responses from the plugin
957          * @throws FcpException
958          *             if an FCP error occurs
959          * @throws IOException
960          *             if an I/O error occurs
961          */
962         public Map<String, String> sendPluginMessage(final String pluginClass, final Map<String, String> parameters, final long dataLength, final InputStream dataInputStream) throws IOException, FcpException {
963                 final Map<String, String> pluginReplies = Collections.synchronizedMap(new HashMap<String, String>());
964                 new ExtendedFcpAdapter() {
965
966                         @SuppressWarnings("synthetic-access")
967                         private final String identifier = createIdentifier("FCPPluginMessage");
968
969                         @Override
970                         @SuppressWarnings("synthetic-access")
971                         public void run() throws IOException {
972                                 FCPPluginMessage fcpPluginMessage = new FCPPluginMessage(pluginClass);
973                                 for (Entry<String, String> parameter : parameters.entrySet()) {
974                                         fcpPluginMessage.setParameter(parameter.getKey(), parameter.getValue());
975                                 }
976                                 fcpPluginMessage.setIdentifier(identifier);
977                                 if ((dataLength > 0) && (dataInputStream != null)) {
978                                         fcpPluginMessage.setDataLength(dataLength);
979                                         fcpPluginMessage.setPayloadInputStream(dataInputStream);
980                                 }
981                                 fcpConnection.sendMessage(fcpPluginMessage);
982                         }
983
984                         /**
985                          * {@inheritDoc}
986                          */
987                         @Override
988                         public void receivedFCPPluginReply(FcpConnection fcpConnection, FCPPluginReply fcpPluginReply) {
989                                 if (!fcpPluginReply.getIdentifier().equals(identifier)) {
990                                         return;
991                                 }
992                                 pluginReplies.putAll(fcpPluginReply.getReplies());
993                                 completionLatch.countDown();
994                         }
995
996                 }.execute();
997                 return pluginReplies;
998         }
999
1000         //
1001         // NODE INFORMATION
1002         //
1003
1004         /**
1005          * Returns information about the node.
1006          *
1007          * @param giveOpennetRef
1008          *            Whether to return the OpenNet reference
1009          * @param withPrivate
1010          *            Whether to return private node data
1011          * @param withVolatile
1012          *            Whether to return volatile node data
1013          * @return Node information
1014          * @throws FcpException
1015          *             if an FCP error occurs
1016          * @throws IOException
1017          *             if an I/O error occurs
1018          */
1019         public NodeData getNodeInformation(final Boolean giveOpennetRef, final Boolean withPrivate, final Boolean withVolatile) throws IOException, FcpException {
1020                 final ObjectWrapper<NodeData> nodeDataWrapper = new ObjectWrapper<NodeData>();
1021                 new ExtendedFcpAdapter() {
1022
1023                         @Override
1024                         @SuppressWarnings("synthetic-access")
1025                         public void run() throws IOException {
1026                                 GetNode getNodeMessage = new GetNode(giveOpennetRef, withPrivate, withVolatile);
1027                                 fcpConnection.sendMessage(getNodeMessage);
1028                         }
1029
1030                         /**
1031                          * {@inheritDoc}
1032                          */
1033                         @Override
1034                         public void receivedNodeData(FcpConnection fcpConnection, NodeData nodeData) {
1035                                 nodeDataWrapper.set(nodeData);
1036                                 completionLatch.countDown();
1037                         }
1038                 }.execute();
1039                 return nodeDataWrapper.get();
1040         }
1041
1042         //
1043         // PRIVATE METHODS
1044         //
1045
1046         /**
1047          * Creates a unique request identifier.
1048          *
1049          * @param basename
1050          *            The basename of the request
1051          * @return The created request identifier
1052          */
1053         private String createIdentifier(String basename) {
1054                 return basename + "-" + System.currentTimeMillis() + "-" + (int) (Math.random() * Integer.MAX_VALUE);
1055         }
1056
1057         /**
1058          * Checks whether the connection is in the required state.
1059          *
1060          * @param connected
1061          *            The required connection state
1062          * @throws FcpException
1063          *             if the connection is not in the required state
1064          */
1065         private void checkConnected(boolean connected) throws FcpException {
1066                 if (this.connected != connected) {
1067                         throw new FcpException("Client is " + (connected ? "not" : "already") + " connected.");
1068                 }
1069         }
1070
1071         /**
1072          * Tells the client that it is now disconnected. This method is called by
1073          * {@link ExtendedFcpAdapter} only.
1074          */
1075         private void setDisconnected() {
1076                 connected = false;
1077         }
1078
1079         /**
1080          * Implementation of an {@link FcpListener} that can store an
1081          * {@link FcpException} and wait for the arrival of a certain command.
1082          *
1083          * @author David ‘Bombe’ Roden &lt;bombe@freenetproject.org&gt;
1084          */
1085         private abstract class ExtendedFcpAdapter extends FcpAdapter {
1086
1087                 /** The count down latch used to wait for completion. */
1088                 protected final CountDownLatch completionLatch = new CountDownLatch(1);
1089
1090                 /** The FCP exception, if any. */
1091                 protected FcpException fcpException;
1092
1093                 /**
1094                  * Creates a new extended FCP adapter.
1095                  */
1096                 public ExtendedFcpAdapter() {
1097                         /* do nothing. */
1098                 }
1099
1100                 /**
1101                  * Executes the FCP commands in {@link #run()}, wrapping the execution
1102                  * and catching exceptions.
1103                  *
1104                  * @throws IOException
1105                  *             if an I/O error occurs
1106                  * @throws FcpException
1107                  *             if an FCP error occurs
1108                  */
1109                 @SuppressWarnings("synthetic-access")
1110                 public void execute() throws IOException, FcpException {
1111                         checkConnected(true);
1112                         fcpConnection.addFcpListener(this);
1113                         try {
1114                                 run();
1115                                 while (true) {
1116                                         try {
1117                                                 completionLatch.await();
1118                                                 break;
1119                                         } catch (InterruptedException ie1) {
1120                                                 /* ignore, we’ll loop. */
1121                                         }
1122                                 }
1123                         } catch (IOException ioe1) {
1124                                 setDisconnected();
1125                                 throw ioe1;
1126                         } finally {
1127                                 fcpConnection.removeFcpListener(this);
1128                         }
1129                         if (fcpException != null) {
1130                                 setDisconnected();
1131                                 throw fcpException;
1132                         }
1133                 }
1134
1135                 /**
1136                  * The FCP commands that actually get executed.
1137                  *
1138                  * @throws IOException
1139                  *             if an I/O error occurs
1140                  */
1141                 public abstract void run() throws IOException;
1142
1143                 /**
1144                  * {@inheritDoc}
1145                  */
1146                 @Override
1147                 public void connectionClosed(FcpConnection fcpConnection, Throwable throwable) {
1148                         fcpException = new FcpException("Connection closed", throwable);
1149                         completionLatch.countDown();
1150                 }
1151
1152                 /**
1153                  * {@inheritDoc}
1154                  */
1155                 @Override
1156                 public void receivedCloseConnectionDuplicateClientName(FcpConnection fcpConnection, CloseConnectionDuplicateClientName closeConnectionDuplicateClientName) {
1157                         fcpException = new FcpException("Connection closed, duplicate client name");
1158                         completionLatch.countDown();
1159                 }
1160
1161                 /**
1162                  * {@inheritDoc}
1163                  */
1164                 @Override
1165                 public void receivedProtocolError(FcpConnection fcpConnection, ProtocolError protocolError) {
1166                         fcpException = new FcpException("Protocol error (" + protocolError.getCode() + ", " + protocolError.getCodeDescription());
1167                         completionLatch.countDown();
1168                 }
1169
1170         }
1171
1172 }