WIP
[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.net.UnknownHostException;
29 import java.util.ArrayList;
30 import java.util.Collections;
31 import java.util.HashMap;
32 import java.util.HashSet;
33 import java.util.Iterator;
34 import java.util.List;
35 import java.util.Map;
36 import java.util.Properties;
37 import java.util.Set;
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>, 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         /** Keeps track of which connection is in use right now. */
76         private Set<HighLevelClient> usedConnections = Collections.synchronizedSet(new HashSet<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 the given node was disconnected.
156          * 
157          * @param node
158          *            The node that is now disconnected
159          * @param throwable
160          *            The exception that caused the disconnect, or <code>null</code>
161          *            if there was no exception
162          */
163         private void fireNodeDisconnected(Node node, Throwable throwable) {
164                 for (NodeListener nodeListener: nodeListeners) {
165                         nodeListener.nodeDisconnected(node, throwable);
166                 }
167         }
168
169         //
170         // ACCESSORS
171         //
172
173         /**
174          * Returns the directory in which the nodes are stored.
175          * 
176          * @return The directory the nodes are stored in
177          */
178         public String getDirectory() {
179                 return directory;
180         }
181
182         /**
183          * Checks whether the given node is already connected.
184          * 
185          * @param node
186          *            The node to check
187          * @return <code>true</code> if the node is already connected,
188          *         <code>false</code> otherwise
189          */
190         public boolean hasNode(Node node) {
191                 return nodes.contains(node);
192         }
193
194         /**
195          * {@inheritDoc}
196          */
197         public Iterator<Node> iterator() {
198                 return nodes.iterator();
199         }
200
201         //
202         // ACTIONS
203         //
204
205         /**
206          * Loads nodes.
207          * 
208          * @throws IOException
209          *             if an I/O error occurs loading the nodes
210          */
211         public void load() throws IOException {
212                 File directoryFile = new File(directory);
213                 File nodeFile = new File(directoryFile, "nodes.properties");
214                 if (!nodeFile.exists() || !nodeFile.isFile() || !nodeFile.canRead()) {
215                         return;
216                 }
217                 Properties nodeProperties = new Properties();
218                 InputStream nodeInputStream = null;
219                 try {
220                         nodeInputStream = new FileInputStream(nodeFile);
221                         nodeProperties.load(nodeInputStream);
222                 } finally {
223                         Closer.close(nodeInputStream);
224                 }
225                 int nodeIndex = -1;
226                 List<Node> loadedNodes = new ArrayList<Node>();
227                 while (nodeProperties.containsKey("nodes." + ++nodeIndex + ".name")) {
228                         String nodePrefix = "nodes." + nodeIndex;
229                         String nodeName = nodeProperties.getProperty(nodePrefix + ".name");
230                         if (!Verifier.verifyNodeName(nodeName)) {
231                                 logger.log(Level.WARNING, "invalid node name “" + nodeName + "”, skipping…");
232                                 continue;
233                         }
234                         String nodeHostname = nodeProperties.getProperty(nodePrefix + ".hostname");
235                         if (!Verifier.verifyHostname(nodeHostname)) {
236                                 logger.log(Level.WARNING, "invalid host name “" + nodeHostname + "”");
237                                 /* not fatal, might be valid later on. */
238                         }
239                         String nodePortString = nodeProperties.getProperty(nodePrefix + ".port");
240                         if (!Verifier.verifyPort(nodePortString)) {
241                                 logger.log(Level.WARNING, "invalid port number “" + nodePortString + "”, skipping…");
242                                 continue;
243                         }
244                         int nodePort = -1;
245                         try {
246                                 nodePort = Integer.valueOf(nodePortString);
247                         } catch (NumberFormatException nfe1) {
248                                 /* shouldn't happen, port number was checked before. */
249                                 logger.log(Level.SEVERE, "invalid port number “" + nodePortString + "”, check failed! skipping…");
250                                 continue;
251                         }
252                         Node newNode = new Node();
253                         newNode.setName(nodeName);
254                         newNode.setHostname(nodeHostname);
255                         newNode.setPort(nodePort);
256                         loadedNodes.add(newNode);
257                 }
258                 logger.fine("loaded " + loadedNodes.size() + " nodes from config");
259                 synchronized (syncObject) {
260                         nodes.clear();
261                         for (Node node: nodes) {
262                                 addNode(node);
263                         }
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          * @throws UnknownHostException
305          *             if the hostname of the node can not be resolved
306          */
307         public void addNode(Node node) throws UnknownHostException {
308                 synchronized (syncObject) {
309                         if (!nodes.contains(node)) {
310                                 HighLevelClient highLevelClient= new HighLevelClient(clientName, node.getHostname(), node.getPort());
311                                 nodes.add(node);
312                                 clientNodes.put(highLevelClient, node);
313                                 nodeClients.put(node, highLevelClient);
314                                 highLevelClient.addHighLevelClientListener(this);
315                                 fireNodeAdded(node);
316                         }
317                 }
318         }
319
320         /**
321          * Removes the given node from the node manager, disconnecting it if it is
322          * currently connected.
323          * 
324          * @param node
325          *            The node to remove
326          */
327         public void removeNode(Node node) {
328                 synchronized (syncObject) {
329                         if (!nodes.contains(node)) {
330                                 return;
331                         }
332                         if (nodeClients.containsKey(node)) {
333                                 disconnect(node);
334                         }
335                         fireNodeRemoved(node);
336                 }
337         }
338
339         /**
340          * Tries to establish a connection with the given node.
341          * 
342          * @param node
343          *            The node to connect to
344          */
345         public void connect(Node node) {
346                 HighLevelClient highLevelClient;
347                 synchronized (syncObject) {
348                         highLevelClient = nodeClients.get(node);
349                 }
350                 try {
351                         highLevelClient.connect();
352                 } catch (IOException ioe1) {
353                         fireNodeDisconnected(node, ioe1);
354                 }
355         }
356
357         /**
358          * Disconnects the given node without removing it.
359          * 
360          * @param node
361          *            The node to disconnect
362          */
363         public void disconnect(Node node) {
364                 synchronized (syncObject) {
365                         if (!nodes.contains(node)) {
366                                 return;
367                         }
368                         HighLevelClient highLevelClient = nodeClients.get(node);
369                         highLevelClient.disconnect();
370                 }
371         }
372
373         /**
374          * Returns a list of all nodes.
375          * 
376          * @return A list of all nodes
377          */
378         public List<Node> getNodes() {
379                 return Collections.unmodifiableList(nodes);
380         }
381
382         /**
383          * “Borrows” a high-level client for the given node. A borrowed client
384          * <strong>has</strong> to be returned to the node manager using
385          * {@link #returnHighLevelClient(HighLevelClient)} when it is no longer in
386          * use, i.e. after a message has been sent! This method will block until a
387          * high-level client for the given node is available.
388          * 
389          * @param node
390          *            The node to get a high-level client for
391          * @return The high-level client for a node, or <code>null</code> if the
392          *         node was disconnected or removed
393          */
394         public HighLevelClient borrowHighLevelClient(Node node) {
395                 synchronized (syncObject) {
396                         if (!nodeClients.containsKey(node)) {
397                                 return null;
398                         }
399                         HighLevelClient highLevelClient = nodeClients.get(node);
400                         while (nodeClients.containsKey(node) && usedConnections.contains(highLevelClient)) {
401                                 try {
402                                         syncObject.wait();
403                                 } catch (InterruptedException ie1) {
404                                         /* ignore. TODO - check. */
405                                 }
406                         }
407                         if (!nodeClients.containsKey(node)) {
408                                 return null;
409                         }
410                         usedConnections.add(highLevelClient);
411                         return highLevelClient;
412                 }
413         }
414
415         /**
416          * Returns a borrowed high-level client.
417          * 
418          * @see #borrowHighLevelClient(Node)
419          * @param highLevelClient
420          *            The high-level client to return
421          */
422         public void returnHighLevelClient(HighLevelClient highLevelClient) {
423                 synchronized (syncObject) {
424                         if (!clientNodes.containsKey(highLevelClient)) {
425                                 return;
426                         }
427                         usedConnections.remove(highLevelClient);
428                         syncObject.notifyAll();
429                 }
430         }
431
432         //
433         // PRIVATE METHODS
434         //
435
436         /**
437          * Finds a currently unused high-level client, optionally waiting until a
438          * client is free and marking it used.
439          * 
440          * @param wait
441          *            <code>true</code> to wait for a free connection,
442          *            <code>false</code> to return <code>null</code>
443          * @param markAsUsed
444          *            <code>true</code> to mark the connection as used before
445          *            returning it, <code>false</code> not to mark it
446          * @return An unused FCP connection, or <code>null</code> if no connection
447          *         could be found
448          */
449         @SuppressWarnings("unused")
450         private HighLevelClient findUnusedClient(boolean wait, boolean markAsUsed) {
451                 synchronized (syncObject) {
452                         HighLevelClient freeHighLevelClient = null;
453                         while (freeHighLevelClient == null) {
454                                 for (HighLevelClient highLevelClient: nodeClients.values()) {
455                                         if (!usedConnections.contains(highLevelClient)) {
456                                                 freeHighLevelClient = highLevelClient;
457                                                 break;
458                                         }
459                                 }
460                                 if (freeHighLevelClient != null) {
461                                         if (markAsUsed) {
462                                                 usedConnections.add(freeHighLevelClient);
463                                         }
464                                         return freeHighLevelClient;
465                                 }
466                                 if (!wait) {
467                                         return null;
468                                 }
469                                 try {
470                                         syncObject.wait();
471                                 } catch (InterruptedException e) {
472                                         /* ignore, just re-check. */
473                                 }
474                         }
475                         /* we never get here, but the compiler doesn't realize. */
476                         return null;
477                 }
478         }
479
480         //
481         // INTERFACE HighLevelClientListener
482         //
483
484         /**
485          * {@inheritDoc}
486          */
487         public void clientConnected(HighLevelClient highLevelClient) {
488                 logger.log(Level.FINER, "clientConnected(c=" + highLevelClient + ")");
489                 Node node = clientNodes.get(highLevelClient);
490                 if (node == null) {
491                         logger.log(Level.WARNING, "got event for unknown client");
492                         return;
493                 }
494                 fireNodeConnected(node);
495         }
496
497         /**
498          * {@inheritDoc}
499          */
500         public void clientDisconnected(HighLevelClient highLevelClient, Throwable throwable) {
501                 logger.log(Level.FINER, "clientDisconnected(c=" + highLevelClient + ",t=" + throwable + ")");
502                 synchronized (syncObject) {
503                         Node node = clientNodes.remove(highLevelClient);
504                         if (node == null) {
505                                 logger.log(Level.WARNING, "got event for unknown client");
506                                 return;
507                         }
508                         nodeClients.remove(node);
509                         usedConnections.remove(highLevelClient);
510                         fireNodeDisconnected(node, throwable);
511                 }
512         }
513
514 }