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