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