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