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