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