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