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