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