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