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