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