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.highlevel.HighLevelClient;
44 import net.pterodactylus.fcp.highlevel.HighLevelClientListener;
45 import net.pterodactylus.fcp.highlevel.HighLevelException;
46 import net.pterodactylus.fcp.highlevel.KeyGenerationResult;
47 import net.pterodactylus.jsite.util.IdGenerator;
48 import net.pterodactylus.util.io.Closer;
49 import net.pterodactylus.util.logging.Logging;
50 import net.pterodactylus.util.number.Hex;
55 * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
57 public class NodeManager implements Iterable<Node>, PropertyChangeListener, HighLevelClientListener {
60 private static final Logger logger = Logging.getLogger(NodeManager.class.getName());
62 /** The FCP client name. */
63 private final String clientName;
65 /** The directory for the configuration. */
66 private final String directory;
68 /** Object used for synchronization. */
69 private final Object syncObject = new Object();
71 /** Node listener support. */
72 private final NodeListenerSupport nodeListenerSupport = new NodeListenerSupport();
75 private final List<Node> nodes = Collections.synchronizedList(new ArrayList<Node>());
77 /** Map from node ID to node. */
78 private final Map<String, Node> idNodes = Collections.synchronizedMap(new HashMap<String, Node>());
80 /** All FCP connections. */
81 private final Map<Node, HighLevelClient> nodeClients = Collections.synchronizedMap(new HashMap<Node, HighLevelClient>());
83 /** Maps nodes to high-level clients. */
84 private final Map<HighLevelClient, Node> clientNodes = Collections.synchronizedMap(new HashMap<HighLevelClient, Node>());
86 /** Collection of currently connected nodes. */
87 private final Set<Node> connectedNodes = Collections.synchronizedSet(new HashSet<Node>());
90 * Creates a new FCP collector.
93 * The name of the FCP client
95 * The directory in which to store the nodes
97 public NodeManager(String clientName, String directory) {
98 this.clientName = clientName;
99 this.directory = directory;
107 * Adds the given listener to the list of listeners.
109 * @param nodeListener
110 * The listener to add
112 public void addNodeListener(NodeListener nodeListener) {
113 nodeListenerSupport.addListener(nodeListener);
117 * Removes the given listener from the list of listeners.
119 * @param nodeListener
120 * The listener to remove
122 public void removeNodeListener(NodeListener nodeListener) {
123 nodeListenerSupport.removeListener(nodeListener);
131 * Returns the directory in which the nodes are stored.
133 * @return The directory the nodes are stored in
135 public String getDirectory() {
140 * Checks whether the given node is already connected.
144 * @return <code>true</code> if the node is already connected,
145 * <code>false</code> otherwise
147 public boolean hasNode(Node node) {
148 return nodes.contains(node);
152 * Returns whether the given node is currently connected.
156 * @return <code>true</code> if the node is currently connected,
157 * <code>false</code> otherwise
159 public boolean isNodeConnected(Node node) {
160 return connectedNodes.contains(node);
166 public Iterator<Node> iterator() {
167 return nodes.iterator();
177 * @throws IOException
178 * if an I/O error occurs loading the nodes
180 public void load() throws IOException {
181 logger.log(Level.FINEST, "load()");
182 File directoryFile = new File(directory);
183 File nodeFile = new File(directoryFile, "nodes.properties");
184 if (!nodeFile.exists() || !nodeFile.isFile() || !nodeFile.canRead()) {
187 Properties nodeProperties = new Properties();
188 InputStream nodeInputStream = null;
190 nodeInputStream = new FileInputStream(nodeFile);
191 nodeProperties.load(nodeInputStream);
193 Closer.close(nodeInputStream);
196 List<Node> loadedNodes = new ArrayList<Node>();
197 while (nodeProperties.containsKey("nodes." + ++nodeIndex + ".name")) {
198 String nodePrefix = "nodes." + nodeIndex;
199 String nodeId = nodeProperties.getProperty(nodePrefix + ".id");
200 if (nodeId == null) {
201 nodeId = Hex.toHex(IdGenerator.generateId());
203 String nodeName = nodeProperties.getProperty(nodePrefix + ".name");
204 if (!Verifier.verifyNodeName(nodeName)) {
205 logger.log(Level.WARNING, "invalid node name “" + nodeName + "”, skipping…");
208 String nodeHostname = nodeProperties.getProperty(nodePrefix + ".hostname");
209 if (!Verifier.verifyHostname(nodeHostname)) {
210 logger.log(Level.WARNING, "invalid host name “" + nodeHostname + "”");
211 /* not fatal, might be valid later on. */
213 String nodePortString = nodeProperties.getProperty(nodePrefix + ".port");
214 if (!Verifier.verifyPort(nodePortString)) {
215 logger.log(Level.WARNING, "invalid port number “" + nodePortString + "”, skipping…");
220 nodePort = Integer.valueOf(nodePortString);
221 } catch (NumberFormatException nfe1) {
222 /* shouldn't happen, port number was checked before. */
223 logger.log(Level.SEVERE, "invalid port number “" + nodePortString + "”, check failed! skipping…");
226 Node newNode = new Node();
227 newNode.setId(nodeId);
228 newNode.setName(nodeName);
229 newNode.setHostname(nodeHostname);
230 newNode.setPort(nodePort);
231 loadedNodes.add(newNode);
233 logger.log(Level.FINE, "loaded " + loadedNodes.size() + " nodes from config");
234 synchronized (syncObject) {
236 for (Node node : loadedNodes) {
243 * Saves all configured nodes.
245 * @throws IOException
246 * if an I/O error occurs saving the nodes
248 public void save() throws IOException {
249 logger.log(Level.FINEST, "save()");
250 File directoryFile = new File(directory);
251 if (!directoryFile.exists()) {
252 if (!directoryFile.mkdirs()) {
253 throw new IOException("could not create directory: " + directory);
256 Properties nodeProperties = new Properties();
258 for (Node node : nodes) {
259 String nodePrefix = "nodes." + ++nodeIndex;
260 nodeProperties.setProperty(nodePrefix + ".id", node.getId());
261 nodeProperties.setProperty(nodePrefix + ".name", node.getName());
262 nodeProperties.setProperty(nodePrefix + ".hostname", node.getHostname());
263 nodeProperties.setProperty(nodePrefix + ".port", String.valueOf(node.getPort()));
265 File projectFile = new File(directoryFile, "nodes.properties");
266 OutputStream nodeOutputStream = null;
268 nodeOutputStream = new FileOutputStream(projectFile);
269 nodeProperties.store(nodeOutputStream, "jSite nodes");
271 Closer.close(nodeOutputStream);
276 * Adds the given node to this manager.
278 * @see #connect(Node)
280 * The node to connect to
281 * @return <code>true</code> if the node was added, <code>false</code> if
282 * the node was not added because it was already known
284 public boolean addNode(Node node) {
285 logger.log(Level.FINEST, "addNode(node=" + node + ")");
286 if (nodes.contains(node)) {
287 logger.log(Level.WARNING, "was told to add already known node: " + node);
290 node.addPropertyChangeListener(this);
291 HighLevelClient highLevelClient = new HighLevelClient(clientName);
293 idNodes.put(node.getId(), node);
294 clientNodes.put(highLevelClient, node);
295 nodeClients.put(node, highLevelClient);
296 highLevelClient.addHighLevelClientListener(this);
297 nodeListenerSupport.fireNodeAdded(node);
302 * Removes the given node from the node manager, disconnecting it if it is
303 * currently connected.
308 public void removeNode(Node node) {
309 logger.log(Level.FINEST, "removeNode(node=" + node + ")");
310 synchronized (syncObject) {
311 if (!nodes.contains(node)) {
314 if (nodeClients.containsKey(node)) {
318 idNodes.remove(node.getId());
319 node.removePropertyChangeListener(this);
320 nodeListenerSupport.fireNodeRemoved(node);
325 * Tries to establish a connection with the given node.
328 * The node to connect to
330 public void connect(Node node) {
331 logger.log(Level.FINEST, "connect(node=" + node + ")");
332 HighLevelClient highLevelClient;
333 highLevelClient = nodeClients.get(node);
334 if (highLevelClient == null) {
335 logger.log(Level.WARNING, "was told to connect to unknown node: " + node);
339 highLevelClient.connect(node.getHostname(), node.getPort());
340 } catch (UnknownHostException uhe1) {
341 nodeListenerSupport.fireNodeConnectionFailed(node, uhe1);
342 } catch (IOException ioe1) {
343 nodeListenerSupport.fireNodeConnectionFailed(node, ioe1);
348 * Disconnects the given node without removing it.
351 * The node to disconnect
353 public void disconnect(Node node) {
354 logger.log(Level.FINEST, "disconnect(node=" + node + ")");
355 synchronized (syncObject) {
356 if (!nodes.contains(node)) {
359 HighLevelClient highLevelClient = nodeClients.get(node);
360 highLevelClient.disconnect();
365 * Returns a list of all nodes.
367 * @return A list of all nodes
369 public List<Node> getNodes() {
370 return Collections.unmodifiableList(nodes);
374 * Returns the high-level client for a given node.
377 * The node to get a high-level client for
378 * @return The high-level client for a node, or <code>null</code> if the
379 * node was disconnected or removed
381 public HighLevelClient getHighLevelClient(Node node) {
382 return nodeClients.get(node);
386 * Returns the node for a high-level client.
388 * @param highLevelClient
389 * The high-level client to get the node for
390 * @return The node for the high-level client, or <code>null</code> if the
391 * high-level client is not known
393 public Node getNode(HighLevelClient highLevelClient) {
394 return clientNodes.get(highLevelClient);
398 * Returns the node identified by the given ID.
402 * @return The node with the given ID, or <code>null</code> if no such node
405 Node getNode(String id) {
406 return idNodes.get(id);
410 * Generates a new SSK key pair.
412 * @return An array with the private key at index <code>0</code> and the
413 * public key at index <code>1</code>
414 * @throws IOException
415 * if an I/O error occurs communicating with the node
416 * @throws JSiteException
417 * if there is a problem with the node
419 public String[] generateKeyPair() throws IOException, JSiteException {
420 logger.log(Level.FINEST, "generateKeyPair()");
421 if (nodes.isEmpty()) {
422 throw new NoNodeException("no node configured");
424 Node node = nodes.get(0);
425 HighLevelClient highLevelClient = nodeClients.get(node);
427 KeyGenerationResult keyGenerationResult = highLevelClient.generateKey().getResult();
428 return new String[] { keyGenerationResult.getInsertURI(), keyGenerationResult.getRequestURI() };
429 } catch (HighLevelException hle1) {
430 throw new BackendException(hle1);
431 } catch (InterruptedException e) {
442 // INTERFACE HighLevelClientListener
448 public void clientConnected(HighLevelClient highLevelClient) {
449 logger.log(Level.FINEST, "clientConnected(highLevelClient=" + highLevelClient + ")");
450 Node node = clientNodes.get(highLevelClient);
452 logger.log(Level.WARNING, "got event for unknown client");
455 connectedNodes.add(node);
456 nodeListenerSupport.fireNodeConnected(node);
462 public void clientDisconnected(HighLevelClient highLevelClient, Throwable throwable) {
463 logger.log(Level.FINEST, "clientDisconnected(highLevelClient=" + highLevelClient + ",throwable=" + throwable + ")");
464 synchronized (syncObject) {
465 Node node = clientNodes.get(highLevelClient);
467 logger.log(Level.WARNING, "got event for unknown client");
470 connectedNodes.remove(node);
471 nodeListenerSupport.fireNodeDisconnected(node, throwable);
476 // INTERFACE PropertyChangeListener
482 public void propertyChange(PropertyChangeEvent propertyChangeEvent) {
483 Object eventSource = propertyChangeEvent.getSource();
484 if (eventSource instanceof Node) {
485 String propertyName = propertyChangeEvent.getPropertyName();
486 if ("hostname".equals(propertyName) || "port".equals(propertyName)) {
487 Node node = (Node) eventSource;
488 HighLevelClient highLevelClient = nodeClients.get(node);
489 if (highLevelClient == null) {
490 logger.log(Level.WARNING, "got property change event for unknown node: " + node);
493 if (highLevelClient.isConnected()) {
494 highLevelClient.disconnect();
496 highLevelClient.connect(node.getHostname(), node.getPort());
497 } catch (UnknownHostException uhe1) {
498 nodeListenerSupport.fireNodeConnectionFailed(node, uhe1);
499 } catch (IOException ioe1) {
500 nodeListenerSupport.fireNodeConnectionFailed(node, ioe1);