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