2 * jSite2 - FcpCollector.java -
3 * Copyright © 2008 David Roden
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.
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.
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.
20 package net.pterodactylus.jsite.core;
22 import java.beans.PropertyChangeEvent;
23 import java.beans.PropertyChangeListener;
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;
38 import java.util.Properties;
40 import java.util.logging.Level;
41 import java.util.logging.Logger;
43 import net.pterodactylus.fcp.SSKKeypair;
44 import net.pterodactylus.fcp.highlevel.FcpClient;
45 import net.pterodactylus.fcp.highlevel.FcpException;
46 import net.pterodactylus.jsite.util.IdGenerator;
47 import net.pterodactylus.util.io.Closer;
48 import net.pterodactylus.util.logging.Logging;
49 import net.pterodactylus.util.number.Hex;
54 * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
56 public class NodeManager implements Iterable<Node>, PropertyChangeListener {
59 private static final Logger logger = Logging.getLogger(NodeManager.class.getName());
61 /** The FCP client name. */
62 private final String clientName;
64 /** The directory for the configuration. */
65 private final String directory;
67 /** Object used for synchronization. */
68 private final Object syncObject = new Object();
70 /** Node listener support. */
71 private final NodeListenerSupport nodeListenerManager = new NodeListenerSupport();
74 private final List<Node> nodes = Collections.synchronizedList(new ArrayList<Node>());
76 /** Map from node ID to node. */
77 private final Map<String, Node> idNodes = Collections.synchronizedMap(new HashMap<String, Node>());
79 /** Map from node to client. */
80 private final Map<Node, FcpClient> nodeClients = Collections.synchronizedMap(new HashMap<Node, FcpClient>());
82 /** Collection of currently connected nodes. */
83 private final Set<Node> connectedNodes = Collections.synchronizedSet(new HashSet<Node>());
86 * Creates a new FCP collector.
89 * The name of the FCP client
91 * The directory in which to store the nodes
93 public NodeManager(String clientName, String directory) {
94 this.clientName = clientName;
95 this.directory = directory;
103 * Adds the given listener to the list of listeners.
105 * @param nodeListener
106 * The listener to add
108 public void addNodeListener(NodeListener nodeListener) {
109 nodeListenerManager.addListener(nodeListener);
113 * Removes the given listener from the list of listeners.
115 * @param nodeListener
116 * The listener to remove
118 public void removeNodeListener(NodeListener nodeListener) {
119 nodeListenerManager.removeListener(nodeListener);
127 * Returns the directory in which the nodes are stored.
129 * @return The directory the nodes are stored in
131 public String getDirectory() {
136 * Checks whether the given node is already connected.
140 * @return <code>true</code> if the node is already connected,
141 * <code>false</code> otherwise
143 public boolean hasNode(Node node) {
144 return nodes.contains(node);
148 * Returns whether the given node is currently connected.
152 * @return <code>true</code> if the node is currently connected,
153 * <code>false</code> otherwise
155 public boolean isNodeConnected(Node node) {
156 return connectedNodes.contains(node);
162 public Iterator<Node> iterator() {
163 return nodes.iterator();
173 * @throws IOException
174 * if an I/O error occurs loading the nodes
176 public void load() throws IOException {
177 logger.log(Level.FINEST, "load()");
178 File directoryFile = new File(directory);
179 File nodeFile = new File(directoryFile, "nodes.properties");
180 if (!nodeFile.exists() || !nodeFile.isFile() || !nodeFile.canRead()) {
183 Properties nodeProperties = new Properties();
184 InputStream nodeInputStream = null;
186 nodeInputStream = new FileInputStream(nodeFile);
187 nodeProperties.load(nodeInputStream);
189 Closer.close(nodeInputStream);
192 List<Node> loadedNodes = new ArrayList<Node>();
193 while (nodeProperties.containsKey("nodes." + ++nodeIndex + ".name")) {
194 String nodePrefix = "nodes." + nodeIndex;
195 String nodeId = nodeProperties.getProperty(nodePrefix + ".id");
196 if (nodeId == null) {
197 nodeId = Hex.toHex(IdGenerator.generateId());
199 String nodeName = nodeProperties.getProperty(nodePrefix + ".name");
200 if (!Verifier.verifyNodeName(nodeName)) {
201 logger.log(Level.WARNING, "invalid node name “" + nodeName + "”, skipping…");
204 String nodeHostname = nodeProperties.getProperty(nodePrefix + ".hostname");
205 if (!Verifier.verifyHostname(nodeHostname)) {
206 logger.log(Level.WARNING, "invalid host name “" + nodeHostname + "”");
207 /* not fatal, might be valid later on. */
209 String nodePortString = nodeProperties.getProperty(nodePrefix + ".port");
210 if (!Verifier.verifyPort(nodePortString)) {
211 logger.log(Level.WARNING, "invalid port number “" + nodePortString + "”, skipping…");
216 nodePort = Integer.valueOf(nodePortString);
217 } catch (NumberFormatException nfe1) {
218 /* shouldn't happen, port number was checked before. */
219 logger.log(Level.SEVERE, "invalid port number “" + nodePortString + "”, check failed! skipping…");
222 Node newNode = new Node();
223 newNode.setId(nodeId);
224 newNode.setName(nodeName);
225 newNode.setHostname(nodeHostname);
226 newNode.setPort(nodePort);
227 loadedNodes.add(newNode);
229 logger.log(Level.FINE, "loaded " + loadedNodes.size() + " nodes from config");
230 synchronized (syncObject) {
232 for (Node node : loadedNodes) {
239 * Saves all configured nodes.
241 * @throws IOException
242 * if an I/O error occurs saving the nodes
244 public void save() throws IOException {
245 logger.log(Level.FINEST, "save()");
246 File directoryFile = new File(directory);
247 if (!directoryFile.exists()) {
248 if (!directoryFile.mkdirs()) {
249 throw new IOException("could not create directory: " + directory);
252 Properties nodeProperties = new Properties();
254 for (Node node : nodes) {
255 String nodePrefix = "nodes." + ++nodeIndex;
256 nodeProperties.setProperty(nodePrefix + ".id", node.getId());
257 nodeProperties.setProperty(nodePrefix + ".name", node.getName());
258 nodeProperties.setProperty(nodePrefix + ".hostname", node.getHostname());
259 nodeProperties.setProperty(nodePrefix + ".port", String.valueOf(node.getPort()));
261 File projectFile = new File(directoryFile, "nodes.properties");
262 OutputStream nodeOutputStream = null;
264 nodeOutputStream = new FileOutputStream(projectFile);
265 nodeProperties.store(nodeOutputStream, "jSite nodes");
267 Closer.close(nodeOutputStream);
272 * Adds the given node to this manager.
274 * @see #connect(Node)
276 * The node to connect to
277 * @return <code>true</code> if the node was added, <code>false</code> if
278 * the node was not added because it was already known
280 public boolean addNode(Node node) {
281 logger.log(Level.FINEST, "addNode(node=" + node + ")");
282 if (nodes.contains(node)) {
283 logger.log(Level.WARNING, "was told to add already known node: " + node);
286 node.addPropertyChangeListener(this);
288 idNodes.put(node.getId(), node);
289 nodeListenerManager.fireNodeAdded(node);
294 * Removes the given node from the node manager, disconnecting it if it is
295 * currently connected.
300 public void removeNode(Node node) {
301 logger.log(Level.FINEST, "removeNode(node=" + node + ")");
302 synchronized (syncObject) {
303 if (!nodes.contains(node)) {
307 idNodes.remove(node.getId());
308 node.removePropertyChangeListener(this);
309 nodeListenerManager.fireNodeRemoved(node);
314 * Tries to establish a connection with the given node.
317 * The node to connect to
319 public void connect(Node node) {
320 logger.log(Level.FINEST, "connect(node=" + node + ")");
321 if (!nodes.contains(node)) {
322 logger.log(Level.WARNING, "Was told to connect to node (" + node + ") I don’t know about!");
326 FcpClient fcpClient = new FcpClient(clientName, node.getHostname(), node.getPort());
328 nodeClients.put(node, fcpClient);
329 nodeListenerManager.fireNodeConnected(node);
330 } catch (UnknownHostException uhe1) {
331 nodeListenerManager.fireNodeConnectionFailed(node, uhe1);
332 } catch (IOException ioe1) {
333 nodeListenerManager.fireNodeConnectionFailed(node, ioe1);
334 } catch (FcpException fe1) {
335 nodeListenerManager.fireNodeConnectionFailed(node, fe1);
340 * Disconnects the given node without removing it.
343 * The node to disconnect
345 public void disconnect(Node node) {
346 logger.log(Level.FINEST, "disconnect(node=" + node + ")");
347 if (!nodes.contains(node)) {
348 logger.log(Level.WARNING, "Was told to disconnect from a node (" + node + ") I don’t know about!");
351 FcpClient fcpClient = nodeClients.remove(node);
352 if (fcpClient == null) {
353 logger.log(Level.WARNING, "No FCP client for node (" + node + ")!");
356 fcpClient.disconnect();
357 nodeListenerManager.fireNodeDisconnected(node, null);
361 * Returns a list of all nodes.
363 * @return A list of all nodes
365 public List<Node> getNodes() {
366 return Collections.unmodifiableList(nodes);
370 * Returns the node identified by the given ID.
374 * @return The node with the given ID, or <code>null</code> if no such node
377 Node getNode(String id) {
378 return idNodes.get(id);
382 * Generates a new SSK key pair.
384 * @return An array with the private key at index <code>0</code> and the
385 * public key at index <code>1</code>
386 * @throws IOException
387 * if an I/O error occurs communicating with the node
388 * @throws JSiteException
389 * if there is no connected node
391 public String[] generateKeyPair() throws IOException, JSiteException {
392 logger.log(Level.FINEST, "generateKeyPair()");
393 if (nodes.isEmpty()) {
394 throw new NoNodeException("no node configured");
396 for (FcpClient fcpClient : nodeClients.values()) {
398 SSKKeypair sskKeypair = fcpClient.generateKeyPair();
399 return new String[] { sskKeypair.getInsertURI(), sskKeypair.getRequestURI() };
400 } catch (FcpException fcpe1) {
401 /* ignore, we’ll throw later on if needs be. */
404 throw new JSiteException("Could not get SSK key pair from any node.");
412 // INTERFACE PropertyChangeListener
418 public void propertyChange(PropertyChangeEvent propertyChangeEvent) {
419 Object eventSource = propertyChangeEvent.getSource();
420 if (eventSource instanceof Node) {
421 String propertyName = propertyChangeEvent.getPropertyName();
422 if ("hostname".equals(propertyName) || "port".equals(propertyName)) {
423 /* TODO - reconnect. */