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