e389f23e9d96cf4ffe392546b355057c3ff57348
[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) || (getFailed.getCode() == 24)) {
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                         @Override
848                         public boolean filterObject(Request request) {
849                                 return request instanceof GetRequest;
850                         }
851                 });
852         }
853
854         /**
855          * Returns all currently visible persistent put requests.
856          *
857          * @param global
858          *            <code>true</code> to return put requests from the global
859          *            queue, <code>false</code> to only show requests from the
860          *            client-local queue
861          * @return All put requests
862          * @throws IOException
863          *             if an I/O error occurs
864          * @throws FcpException
865          *             if an FCP error occurs
866          */
867         public Collection<Request> getPutRequests(final boolean global) throws IOException, FcpException {
868                 return Filters.filteredCollection(getRequests(global), new Filter<Request>() {
869
870                         /**
871                          * {@inheritDoc}
872                          */
873                         @Override
874                         public boolean filterObject(Request request) {
875                                 return request instanceof PutRequest;
876                         }
877                 });
878         }
879
880         /**
881          * Returns all currently visible persistent requests.
882          *
883          * @param global
884          *            <code>true</code> to return requests from the global queue,
885          *            <code>false</code> to only show requests from the client-local
886          *            queue
887          * @return All requests
888          * @throws IOException
889          *             if an I/O error occurs
890          * @throws FcpException
891          *             if an FCP error occurs
892          */
893         public Collection<Request> getRequests(final boolean global) throws IOException, FcpException {
894                 final Map<String, Request> requests = Collections.synchronizedMap(new HashMap<String, Request>());
895                 new ExtendedFcpAdapter() {
896
897                         /**
898                          * {@inheritDoc}
899                          */
900                         @Override
901                         @SuppressWarnings("synthetic-access")
902                         public void run() throws IOException {
903                                 fcpConnection.sendMessage(new ListPersistentRequests());
904                         }
905
906                         /**
907                          * {@inheritDoc}
908                          */
909                         @Override
910                         public void receivedPersistentGet(FcpConnection fcpConnection, PersistentGet persistentGet) {
911                                 if (!persistentGet.isGlobal() || global) {
912                                         GetRequest getRequest = new GetRequest(persistentGet);
913                                         requests.put(persistentGet.getIdentifier(), getRequest);
914                                 }
915                         }
916
917                         /**
918                          * {@inheritDoc}
919                          *
920                          * @see net.pterodactylus.fcp.FcpAdapter#receivedDataFound(net.pterodactylus.fcp.FcpConnection,
921                          *      net.pterodactylus.fcp.DataFound)
922                          */
923                         @Override
924                         public void receivedDataFound(FcpConnection fcpConnection, DataFound dataFound) {
925                                 Request getRequest = requests.get(dataFound.getIdentifier());
926                                 if (getRequest == null) {
927                                         return;
928                                 }
929                                 getRequest.setComplete(true);
930                                 getRequest.setLength(dataFound.getDataLength());
931                                 getRequest.setContentType(dataFound.getMetadataContentType());
932                         }
933
934                         /**
935                          * {@inheritDoc}
936                          *
937                          * @see net.pterodactylus.fcp.FcpAdapter#receivedGetFailed(net.pterodactylus.fcp.FcpConnection,
938                          *      net.pterodactylus.fcp.GetFailed)
939                          */
940                         @Override
941                         public void receivedGetFailed(FcpConnection fcpConnection, GetFailed getFailed) {
942                                 Request getRequest = requests.get(getFailed.getIdentifier());
943                                 if (getRequest == null) {
944                                         return;
945                                 }
946                                 getRequest.setComplete(true);
947                                 getRequest.setFailed(true);
948                                 getRequest.setFatal(getFailed.isFatal());
949                                 getRequest.setErrorCode(getFailed.getCode());
950                         }
951
952                         /**
953                          * {@inheritDoc}
954                          *
955                          * @see net.pterodactylus.fcp.FcpAdapter#receivedPersistentPut(net.pterodactylus.fcp.FcpConnection,
956                          *      net.pterodactylus.fcp.PersistentPut)
957                          */
958                         @Override
959                         public void receivedPersistentPut(FcpConnection fcpConnection, PersistentPut persistentPut) {
960                                 if (!persistentPut.isGlobal() || global) {
961                                         PutRequest putRequest = new PutRequest(persistentPut);
962                                         requests.put(persistentPut.getIdentifier(), putRequest);
963                                 }
964                         }
965
966                         /**
967                          * {@inheritDoc}
968                          *
969                          * @see net.pterodactylus.fcp.FcpAdapter#receivedSimpleProgress(net.pterodactylus.fcp.FcpConnection,
970                          *      net.pterodactylus.fcp.SimpleProgress)
971                          */
972                         @Override
973                         public void receivedSimpleProgress(FcpConnection fcpConnection, SimpleProgress simpleProgress) {
974                                 Request request = requests.get(simpleProgress.getIdentifier());
975                                 if (request == null) {
976                                         return;
977                                 }
978                                 request.setTotalBlocks(simpleProgress.getTotal());
979                                 request.setRequiredBlocks(simpleProgress.getRequired());
980                                 request.setFailedBlocks(simpleProgress.getFailed());
981                                 request.setFatallyFailedBlocks(simpleProgress.getFatallyFailed());
982                                 request.setSucceededBlocks(simpleProgress.getSucceeded());
983                                 request.setFinalizedTotal(simpleProgress.isFinalizedTotal());
984                         }
985
986                         /**
987                          * {@inheritDoc}
988                          */
989                         @Override
990                         public void receivedEndListPersistentRequests(FcpConnection fcpConnection, EndListPersistentRequests endListPersistentRequests) {
991                                 completionLatch.countDown();
992                         }
993                 }.execute();
994                 return requests.values();
995         }
996
997         /**
998          * Sends a message to a plugin and waits for the response.
999          *
1000          * @param pluginClass
1001          *            The name of the plugin class
1002          * @param parameters
1003          *            The parameters for the plugin
1004          * @return The responses from the plugin
1005          * @throws FcpException
1006          *             if an FCP error occurs
1007          * @throws IOException
1008          *             if an I/O error occurs
1009          */
1010         public Map<String, String> sendPluginMessage(String pluginClass, Map<String, String> parameters) throws IOException, FcpException {
1011                 return sendPluginMessage(pluginClass, parameters, 0, null);
1012         }
1013
1014         /**
1015          * Sends a message to a plugin and waits for the response.
1016          *
1017          * @param pluginClass
1018          *            The name of the plugin class
1019          * @param parameters
1020          *            The parameters for the plugin
1021          * @param dataLength
1022          *            The length of the optional data stream, or {@code 0} if there
1023          *            is no optional data stream
1024          * @param dataInputStream
1025          *            The input stream for the payload, or {@code null} if there is
1026          *            no payload
1027          * @return The responses from the plugin
1028          * @throws FcpException
1029          *             if an FCP error occurs
1030          * @throws IOException
1031          *             if an I/O error occurs
1032          */
1033         public Map<String, String> sendPluginMessage(final String pluginClass, final Map<String, String> parameters, final long dataLength, final InputStream dataInputStream) throws IOException, FcpException {
1034                 final Map<String, String> pluginReplies = Collections.synchronizedMap(new HashMap<String, String>());
1035                 new ExtendedFcpAdapter() {
1036
1037                         @SuppressWarnings("synthetic-access")
1038                         private final String identifier = createIdentifier("FCPPluginMessage");
1039
1040                         @Override
1041                         @SuppressWarnings("synthetic-access")
1042                         public void run() throws IOException {
1043                                 FCPPluginMessage fcpPluginMessage = new FCPPluginMessage(pluginClass);
1044                                 for (Entry<String, String> parameter : parameters.entrySet()) {
1045                                         fcpPluginMessage.setParameter(parameter.getKey(), parameter.getValue());
1046                                 }
1047                                 fcpPluginMessage.setIdentifier(identifier);
1048                                 if ((dataLength > 0) && (dataInputStream != null)) {
1049                                         fcpPluginMessage.setDataLength(dataLength);
1050                                         fcpPluginMessage.setPayloadInputStream(dataInputStream);
1051                                 }
1052                                 fcpConnection.sendMessage(fcpPluginMessage);
1053                         }
1054
1055                         /**
1056                          * {@inheritDoc}
1057                          */
1058                         @Override
1059                         public void receivedFCPPluginReply(FcpConnection fcpConnection, FCPPluginReply fcpPluginReply) {
1060                                 if (!fcpPluginReply.getIdentifier().equals(identifier)) {
1061                                         return;
1062                                 }
1063                                 pluginReplies.putAll(fcpPluginReply.getReplies());
1064                                 completionLatch.countDown();
1065                         }
1066
1067                 }.execute();
1068                 return pluginReplies;
1069         }
1070
1071         //
1072         // NODE INFORMATION
1073         //
1074
1075         /**
1076          * Returns information about the node.
1077          *
1078          * @param giveOpennetRef
1079          *            Whether to return the OpenNet reference
1080          * @param withPrivate
1081          *            Whether to return private node data
1082          * @param withVolatile
1083          *            Whether to return volatile node data
1084          * @return Node information
1085          * @throws FcpException
1086          *             if an FCP error occurs
1087          * @throws IOException
1088          *             if an I/O error occurs
1089          */
1090         public NodeData getNodeInformation(final Boolean giveOpennetRef, final Boolean withPrivate, final Boolean withVolatile) throws IOException, FcpException {
1091                 final ObjectWrapper<NodeData> nodeDataWrapper = new ObjectWrapper<NodeData>();
1092                 new ExtendedFcpAdapter() {
1093
1094                         @Override
1095                         @SuppressWarnings("synthetic-access")
1096                         public void run() throws IOException {
1097                                 GetNode getNodeMessage = new GetNode(giveOpennetRef, withPrivate, withVolatile);
1098                                 fcpConnection.sendMessage(getNodeMessage);
1099                         }
1100
1101                         /**
1102                          * {@inheritDoc}
1103                          */
1104                         @Override
1105                         public void receivedNodeData(FcpConnection fcpConnection, NodeData nodeData) {
1106                                 nodeDataWrapper.set(nodeData);
1107                                 completionLatch.countDown();
1108                         }
1109                 }.execute();
1110                 return nodeDataWrapper.get();
1111         }
1112
1113         //
1114         // PRIVATE METHODS
1115         //
1116
1117         /**
1118          * Creates a unique request identifier.
1119          *
1120          * @param basename
1121          *            The basename of the request
1122          * @return The created request identifier
1123          */
1124         private String createIdentifier(String basename) {
1125                 return basename + "-" + System.currentTimeMillis() + "-" + (int) (Math.random() * Integer.MAX_VALUE);
1126         }
1127
1128         /**
1129          * Checks whether the connection is in the required state.
1130          *
1131          * @param connected
1132          *            The required connection state
1133          * @throws FcpException
1134          *             if the connection is not in the required state
1135          */
1136         private void checkConnected(boolean connected) throws FcpException {
1137                 if (this.connected != connected) {
1138                         throw new FcpException("Client is " + (connected ? "not" : "already") + " connected.");
1139                 }
1140         }
1141
1142         /**
1143          * Tells the client that it is now disconnected. This method is called by
1144          * {@link ExtendedFcpAdapter} only.
1145          */
1146         private void setDisconnected() {
1147                 connected = false;
1148         }
1149
1150         /**
1151          * Implementation of an {@link FcpListener} that can store an
1152          * {@link FcpException} and wait for the arrival of a certain command.
1153          *
1154          * @author David ‘Bombe’ Roden &lt;bombe@freenetproject.org&gt;
1155          */
1156         private abstract class ExtendedFcpAdapter extends FcpAdapter {
1157
1158                 /** The count down latch used to wait for completion. */
1159                 protected final CountDownLatch completionLatch = new CountDownLatch(1);
1160
1161                 /** The FCP exception, if any. */
1162                 protected FcpException fcpException;
1163
1164                 /**
1165                  * Creates a new extended FCP adapter.
1166                  */
1167                 public ExtendedFcpAdapter() {
1168                         /* do nothing. */
1169                 }
1170
1171                 /**
1172                  * Executes the FCP commands in {@link #run()}, wrapping the execution
1173                  * and catching exceptions.
1174                  *
1175                  * @throws IOException
1176                  *             if an I/O error occurs
1177                  * @throws FcpException
1178                  *             if an FCP error occurs
1179                  */
1180                 @SuppressWarnings("synthetic-access")
1181                 public void execute() throws IOException, FcpException {
1182                         checkConnected(true);
1183                         fcpConnection.addFcpListener(this);
1184                         try {
1185                                 run();
1186                                 while (true) {
1187                                         try {
1188                                                 completionLatch.await();
1189                                                 break;
1190                                         } catch (InterruptedException ie1) {
1191                                                 /* ignore, we’ll loop. */
1192                                         }
1193                                 }
1194                         } catch (IOException ioe1) {
1195                                 setDisconnected();
1196                                 throw ioe1;
1197                         } finally {
1198                                 fcpConnection.removeFcpListener(this);
1199                         }
1200                         if (fcpException != null) {
1201                                 setDisconnected();
1202                                 throw fcpException;
1203                         }
1204                 }
1205
1206                 /**
1207                  * The FCP commands that actually get executed.
1208                  *
1209                  * @throws IOException
1210                  *             if an I/O error occurs
1211                  */
1212                 public abstract void run() throws IOException;
1213
1214                 /**
1215                  * {@inheritDoc}
1216                  */
1217                 @Override
1218                 public void connectionClosed(FcpConnection fcpConnection, Throwable throwable) {
1219                         fcpException = new FcpException("Connection closed", throwable);
1220                         completionLatch.countDown();
1221                 }
1222
1223                 /**
1224                  * {@inheritDoc}
1225                  */
1226                 @Override
1227                 public void receivedCloseConnectionDuplicateClientName(FcpConnection fcpConnection, CloseConnectionDuplicateClientName closeConnectionDuplicateClientName) {
1228                         fcpException = new FcpException("Connection closed, duplicate client name");
1229                         completionLatch.countDown();
1230                 }
1231
1232                 /**
1233                  * {@inheritDoc}
1234                  */
1235                 @Override
1236                 public void receivedProtocolError(FcpConnection fcpConnection, ProtocolError protocolError) {
1237                         fcpException = new FcpException("Protocol error (" + protocolError.getCode() + ", " + protocolError.getCodeDescription());
1238                         completionLatch.countDown();
1239                 }
1240
1241         }
1242
1243 }