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