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