really remove node
[jSite2.git] / src / net / pterodactylus / jsite / core / NodeManager.java
1 /*
2  * jSite2 - FcpCollector.java -
3  * Copyright © 2008 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.jsite.core;
21
22 import java.beans.PropertyChangeEvent;
23 import java.beans.PropertyChangeListener;
24 import java.io.File;
25 import java.io.FileInputStream;
26 import java.io.FileOutputStream;
27 import java.io.IOException;
28 import java.io.InputStream;
29 import java.io.OutputStream;
30 import java.net.UnknownHostException;
31 import java.util.ArrayList;
32 import java.util.Collections;
33 import java.util.HashMap;
34 import java.util.Iterator;
35 import java.util.List;
36 import java.util.Map;
37 import java.util.Properties;
38 import java.util.logging.Level;
39 import java.util.logging.Logger;
40
41 import net.pterodactylus.fcp.highlevel.HighLevelClient;
42 import net.pterodactylus.fcp.highlevel.HighLevelClientListener;
43 import net.pterodactylus.fcp.highlevel.HighLevelException;
44 import net.pterodactylus.fcp.highlevel.KeyGenerationResult;
45 import net.pterodactylus.util.io.Closer;
46 import net.pterodactylus.util.logging.Logging;
47
48 /**
49  * TODO
50  * 
51  * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
52  * @version $Id$
53  */
54 public class NodeManager implements Iterable<Node>, PropertyChangeListener, HighLevelClientListener {
55
56         /** Logger. */
57         private static final Logger logger = Logging.getLogger(NodeManager.class.getName());
58
59         /** The FCP client name. */
60         private final String clientName;
61
62         /** The directory for the configuration. */
63         private final String directory;
64
65         /** Object used for synchronization. */
66         private final Object syncObject = new Object();
67
68         /** Node listeners. */
69         private List<NodeListener> nodeListeners = Collections.synchronizedList(new ArrayList<NodeListener>());
70
71         /** All nodes. */
72         private List<Node> nodes = Collections.synchronizedList(new ArrayList<Node>());
73
74         /** All FCP connections. */
75         private Map<Node, HighLevelClient> nodeClients = Collections.synchronizedMap(new HashMap<Node, HighLevelClient>());
76
77         /** Maps nodes to high-level clients. */
78         private Map<HighLevelClient, Node> clientNodes = Collections.synchronizedMap(new HashMap<HighLevelClient, Node>());
79
80         /**
81          * Creates a new FCP collector.
82          * 
83          * @param clientName
84          *            The name of the FCP client
85          * @param directory
86          *            The directory in which to store the nodes
87          */
88         public NodeManager(String clientName, String directory) {
89                 this.clientName = clientName;
90                 this.directory = directory;
91         }
92
93         //
94         // EVENT MANAGEMENT
95         //
96
97         /**
98          * Adds the given listener to the list of listeners.
99          * 
100          * @param nodeListener
101          *            The listener to add
102          */
103         public void addNodeListener(NodeListener nodeListener) {
104                 nodeListeners.add(nodeListener);
105         }
106
107         /**
108          * Removes the given listener from the list of listeners.
109          * 
110          * @param nodeListener
111          *            The listener to remove
112          */
113         public void removeNodeListener(NodeListener nodeListener) {
114                 nodeListeners.remove(nodeListener);
115         }
116
117         /**
118          * Notifies all listeners that a node was added.
119          * 
120          * @param node
121          *            The node that was added.
122          */
123         private void fireNodeAdded(Node node) {
124                 for (NodeListener nodeListener: nodeListeners) {
125                         nodeListener.nodeAdded(node);
126                 }
127         }
128
129         /**
130          * Notifies all listeners that a node was removed.
131          * 
132          * @param node
133          *            The node that was removed
134          */
135         private void fireNodeRemoved(Node node) {
136                 for (NodeListener nodeListener: nodeListeners) {
137                         nodeListener.nodeRemoved(node);
138                 }
139         }
140
141         /**
142          * Notifies all listeners that the given node was connected.
143          * 
144          * @param node
145          *            The node that is now connected
146          */
147         private void fireNodeConnected(Node node) {
148                 for (NodeListener nodeListener: nodeListeners) {
149                         nodeListener.nodeConnected(node);
150                 }
151         }
152
153         /**
154          * Notifies all listeners that a connection to a node has failed.
155          * 
156          * @param node
157          *            The node that could not be connected
158          * @param cause
159          *            The cause of the failure
160          */
161         private void fireNodeConnectionFailed(Node node, Throwable cause) {
162                 for (NodeListener nodeListener: nodeListeners) {
163                         nodeListener.nodeConnectionFailed(node, cause);
164                 }
165         }
166
167         /**
168          * Notifies all listeners that the given node was disconnected.
169          * 
170          * @param node
171          *            The node that is now disconnected
172          * @param throwable
173          *            The exception that caused the disconnect, or <code>null</code>
174          *            if there was no exception
175          */
176         private void fireNodeDisconnected(Node node, Throwable throwable) {
177                 for (NodeListener nodeListener: nodeListeners) {
178                         nodeListener.nodeDisconnected(node, throwable);
179                 }
180         }
181
182         //
183         // ACCESSORS
184         //
185
186         /**
187          * Returns the directory in which the nodes are stored.
188          * 
189          * @return The directory the nodes are stored in
190          */
191         public String getDirectory() {
192                 return directory;
193         }
194
195         /**
196          * Checks whether the given node is already connected.
197          * 
198          * @param node
199          *            The node to check
200          * @return <code>true</code> if the node is already connected,
201          *         <code>false</code> otherwise
202          */
203         public boolean hasNode(Node node) {
204                 return nodes.contains(node);
205         }
206
207         /**
208          * {@inheritDoc}
209          */
210         public Iterator<Node> iterator() {
211                 return nodes.iterator();
212         }
213
214         //
215         // ACTIONS
216         //
217
218         /**
219          * Loads nodes.
220          * 
221          * @throws IOException
222          *             if an I/O error occurs loading the nodes
223          */
224         public void load() throws IOException {
225                 File directoryFile = new File(directory);
226                 File nodeFile = new File(directoryFile, "nodes.properties");
227                 if (!nodeFile.exists() || !nodeFile.isFile() || !nodeFile.canRead()) {
228                         return;
229                 }
230                 Properties nodeProperties = new Properties();
231                 InputStream nodeInputStream = null;
232                 try {
233                         nodeInputStream = new FileInputStream(nodeFile);
234                         nodeProperties.load(nodeInputStream);
235                 } finally {
236                         Closer.close(nodeInputStream);
237                 }
238                 int nodeIndex = -1;
239                 List<Node> loadedNodes = new ArrayList<Node>();
240                 while (nodeProperties.containsKey("nodes." + ++nodeIndex + ".name")) {
241                         String nodePrefix = "nodes." + nodeIndex;
242                         String nodeName = nodeProperties.getProperty(nodePrefix + ".name");
243                         if (!Verifier.verifyNodeName(nodeName)) {
244                                 logger.log(Level.WARNING, "invalid node name “" + nodeName + "”, skipping…");
245                                 continue;
246                         }
247                         String nodeHostname = nodeProperties.getProperty(nodePrefix + ".hostname");
248                         if (!Verifier.verifyHostname(nodeHostname)) {
249                                 logger.log(Level.WARNING, "invalid host name “" + nodeHostname + "”");
250                                 /* not fatal, might be valid later on. */
251                         }
252                         String nodePortString = nodeProperties.getProperty(nodePrefix + ".port");
253                         if (!Verifier.verifyPort(nodePortString)) {
254                                 logger.log(Level.WARNING, "invalid port number “" + nodePortString + "”, skipping…");
255                                 continue;
256                         }
257                         int nodePort = -1;
258                         try {
259                                 nodePort = Integer.valueOf(nodePortString);
260                         } catch (NumberFormatException nfe1) {
261                                 /* shouldn't happen, port number was checked before. */
262                                 logger.log(Level.SEVERE, "invalid port number “" + nodePortString + "”, check failed! skipping…");
263                                 continue;
264                         }
265                         Node newNode = new Node();
266                         newNode.setName(nodeName);
267                         newNode.setHostname(nodeHostname);
268                         newNode.setPort(nodePort);
269                         loadedNodes.add(newNode);
270                 }
271                 logger.fine("loaded " + loadedNodes.size() + " nodes from config");
272                 synchronized (syncObject) {
273                         nodes.clear();
274                         for (Node node: loadedNodes) {
275                                 addNode(node);
276                         }
277                 }
278         }
279
280         /**
281          * Saves all configured nodes.
282          * 
283          * @throws IOException
284          *             if an I/O error occurs saving the nodes
285          */
286         public void save() throws IOException {
287                 File directoryFile = new File(directory);
288                 if (!directoryFile.exists()) {
289                         if (!directoryFile.mkdirs()) {
290                                 throw new IOException("could not create directory: " + directory);
291                         }
292                 }
293                 Properties nodeProperties = new Properties();
294                 int nodeIndex = -1;
295                 for (Node node: nodes) {
296                         String nodePrefix = "nodes." + ++nodeIndex;
297                         nodeProperties.setProperty(nodePrefix + ".name", node.getName());
298                         nodeProperties.setProperty(nodePrefix + ".hostname", node.getHostname());
299                         nodeProperties.setProperty(nodePrefix + ".port", String.valueOf(node.getPort()));
300                 }
301                 File projectFile = new File(directoryFile, "nodes.properties");
302                 OutputStream nodeOutputStream = null;
303                 try {
304                         nodeOutputStream = new FileOutputStream(projectFile);
305                         nodeProperties.store(nodeOutputStream, "jSite nodes");
306                 } finally {
307                         Closer.close(nodeOutputStream);
308                 }
309         }
310
311         /**
312          * Adds the given node to this manager.
313          * 
314          * @see #connect(Node)
315          * @param node
316          *            The node to connect to
317          * @return <code>true</code> if the node was added, <code>false</code>
318          *         if the node was not added because it was already known
319          */
320         public boolean addNode(Node node) {
321                 if (nodes.contains(node)) {
322                         logger.warning("was told to add already known node: " + node);
323                         return false;
324                 }
325                 node.addPropertyChangeListener(this);
326                 HighLevelClient highLevelClient = new HighLevelClient(clientName);
327                 nodes.add(node);
328                 clientNodes.put(highLevelClient, node);
329                 nodeClients.put(node, highLevelClient);
330                 highLevelClient.addHighLevelClientListener(this);
331                 fireNodeAdded(node);
332                 return true;
333         }
334
335         /**
336          * Removes the given node from the node manager, disconnecting it if it is
337          * currently connected.
338          * 
339          * @param node
340          *            The node to remove
341          */
342         public void removeNode(Node node) {
343                 synchronized (syncObject) {
344                         if (!nodes.contains(node)) {
345                                 return;
346                         }
347                         if (nodeClients.containsKey(node)) {
348                                 disconnect(node);
349                         }
350                         nodes.remove(node);
351                         node.removePropertyChangeListener(this);
352                         fireNodeRemoved(node);
353                 }
354         }
355
356         /**
357          * Tries to establish a connection with the given node.
358          * 
359          * @param node
360          *            The node to connect to
361          */
362         public void connect(Node node) {
363                 HighLevelClient highLevelClient;
364                 highLevelClient = nodeClients.get(node);
365                 if (highLevelClient == null) {
366                         logger.warning("was told to connect to unknown node: " + node);
367                         return;
368                 }
369                 try {
370                         highLevelClient.connect(node.getHostname(), node.getPort());
371                 } catch (UnknownHostException uhe1) {
372                         fireNodeConnectionFailed(node, uhe1);
373                 } catch (IOException ioe1) {
374                         fireNodeConnectionFailed(node, ioe1);
375                 }
376         }
377
378         /**
379          * Disconnects the given node without removing it.
380          * 
381          * @param node
382          *            The node to disconnect
383          */
384         public void disconnect(Node node) {
385                 synchronized (syncObject) {
386                         if (!nodes.contains(node)) {
387                                 return;
388                         }
389                         HighLevelClient highLevelClient = nodeClients.get(node);
390                         highLevelClient.disconnect();
391                 }
392         }
393
394         /**
395          * Returns a list of all nodes.
396          * 
397          * @return A list of all nodes
398          */
399         public List<Node> getNodes() {
400                 return Collections.unmodifiableList(nodes);
401         }
402
403         /**
404          * Returns the high-level client for a given node.
405          * 
406          * @param node
407          *            The node to get a high-level client for
408          * @return The high-level client for a node, or <code>null</code> if the
409          *         node was disconnected or removed
410          */
411         public HighLevelClient getHighLevelClient(Node node) {
412                 return nodeClients.get(node);
413         }
414
415         /**
416          * Returns the node for a high-level client.
417          * 
418          * @param highLevelClient
419          *            The high-level client to get the node for
420          * @return The node for the high-level client, or <code>null</code> if the
421          *         high-level client is not known
422          */
423         public Node getNode(HighLevelClient highLevelClient) {
424                 return clientNodes.get(highLevelClient);
425         }
426
427         /**
428          * Generates a new SSK key pair.
429          * 
430          * @return An array with the private key at index <code>0</code> and the
431          *         public key at index <code>1</code>
432          * @throws IOException
433          *             if an I/O error occurs communicating with the node
434          * @throws JSiteException
435          *             if there is a problem with the node
436          */
437         public String[] generateKeyPair() throws IOException, JSiteException {
438                 if (nodes.isEmpty()) {
439                         throw new NoNodeException("no node configured");
440                 }
441                 Node node = nodes.get(0);
442                 HighLevelClient highLevelClient = nodeClients.get(node);
443                 try {
444                         KeyGenerationResult keyGenerationResult = highLevelClient.generateKey().getResult();
445                         return new String[] { keyGenerationResult.getInsertURI(), keyGenerationResult.getRequestURI() };
446                 } catch (HighLevelException hle1) {
447                         throw new BackendException(hle1);
448                 } catch (InterruptedException e) {
449                         /* ignore. */
450                 }
451                 return null;
452         }
453
454         //
455         // PRIVATE METHODS
456         //
457
458         //
459         // INTERFACE HighLevelClientListener
460         //
461
462         /**
463          * {@inheritDoc}
464          */
465         public void clientConnected(HighLevelClient highLevelClient) {
466                 logger.log(Level.FINER, "clientConnected(c=" + highLevelClient + ")");
467                 Node node = clientNodes.get(highLevelClient);
468                 if (node == null) {
469                         logger.log(Level.WARNING, "got event for unknown client");
470                         return;
471                 }
472                 fireNodeConnected(node);
473         }
474
475         /**
476          * {@inheritDoc}
477          */
478         public void clientDisconnected(HighLevelClient highLevelClient, Throwable throwable) {
479                 logger.log(Level.FINER, "clientDisconnected(c=" + highLevelClient + ",t=" + throwable + ")");
480                 synchronized (syncObject) {
481                         Node node = clientNodes.get(highLevelClient);
482                         if (node == null) {
483                                 logger.log(Level.WARNING, "got event for unknown client");
484                                 return;
485                         }
486                         fireNodeDisconnected(node, throwable);
487                 }
488         }
489
490         //
491         // INTERFACE PropertyChangeListener
492         //
493
494         /**
495          * {@inheritDoc}
496          */
497         public void propertyChange(PropertyChangeEvent propertyChangeEvent) {
498                 Object eventSource = propertyChangeEvent.getSource();
499                 if (eventSource instanceof Node) {
500                         String propertyName = propertyChangeEvent.getPropertyName();
501                         if ("hostname".equals(propertyName) || "port".equals(propertyName)) {
502                                 Node node = (Node) eventSource;
503                                 HighLevelClient highLevelClient = nodeClients.get(node);
504                                 if (highLevelClient == null) {
505                                         logger.log(Level.WARNING, "got property change event for unknown node: " + node);
506                                         return;
507                                 }
508                                 if (highLevelClient.isConnected()) {
509                                         highLevelClient.disconnect();
510                                         try {
511                                                 highLevelClient.connect(node.getHostname(), node.getPort());
512                                         } catch (UnknownHostException uhe1) {
513                                                 fireNodeConnectionFailed(node, uhe1);
514                                         } catch (IOException ioe1) {
515                                                 fireNodeConnectionFailed(node, ioe1);
516                                         }
517                                 }
518                         }
519                 }
520         }
521
522 }