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