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