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