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