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