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