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.Iterator;
35 import java.util.List;
37 import java.util.Properties;
38 import java.util.logging.Level;
39 import java.util.logging.Logger;
41 import net.pterodactylus.fcp.highlevel.HighLevelClient;
42 import net.pterodactylus.fcp.highlevel.HighLevelClientListener;
43 import net.pterodactylus.fcp.highlevel.HighLevelException;
44 import net.pterodactylus.fcp.highlevel.KeyGenerationResult;
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;
53 * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
55 public class NodeManager implements Iterable<Node>, PropertyChangeListener, HighLevelClientListener {
58 private static final Logger logger = Logging.getLogger(NodeManager.class.getName());
60 /** The FCP client name. */
61 private final String clientName;
63 /** The directory for the configuration. */
64 private final String directory;
66 /** Object used for synchronization. */
67 private final Object syncObject = new Object();
69 /** Node listeners. */
70 private List<NodeListener> nodeListeners = Collections.synchronizedList(new ArrayList<NodeListener>());
73 private List<Node> nodes = Collections.synchronizedList(new ArrayList<Node>());
75 /** Map from node ID to node. */
76 private Map<String, Node> idNodes = Collections.synchronizedMap(new HashMap<String, Node>());
78 /** All FCP connections. */
79 private Map<Node, HighLevelClient> nodeClients = Collections.synchronizedMap(new HashMap<Node, HighLevelClient>());
81 /** Maps nodes to high-level clients. */
82 private Map<HighLevelClient, Node> clientNodes = Collections.synchronizedMap(new HashMap<HighLevelClient, Node>());
85 * Creates a new FCP collector.
88 * The name of the FCP client
90 * The directory in which to store the nodes
92 public NodeManager(String clientName, String directory) {
93 this.clientName = clientName;
94 this.directory = directory;
102 * Adds the given listener to the list of listeners.
104 * @param nodeListener
105 * The listener to add
107 public void addNodeListener(NodeListener nodeListener) {
108 nodeListeners.add(nodeListener);
112 * Removes the given listener from the list of listeners.
114 * @param nodeListener
115 * The listener to remove
117 public void removeNodeListener(NodeListener nodeListener) {
118 nodeListeners.remove(nodeListener);
122 * Notifies all listeners that a node was added.
125 * The node that was added.
127 private void fireNodeAdded(Node node) {
128 for (NodeListener nodeListener : nodeListeners) {
129 nodeListener.nodeAdded(node);
134 * Notifies all listeners that a node was removed.
137 * The node that was removed
139 private void fireNodeRemoved(Node node) {
140 for (NodeListener nodeListener : nodeListeners) {
141 nodeListener.nodeRemoved(node);
146 * Notifies all listeners that the given node was connected.
149 * The node that is now connected
151 private void fireNodeConnected(Node node) {
152 for (NodeListener nodeListener : nodeListeners) {
153 nodeListener.nodeConnected(node);
158 * Notifies all listeners that a connection to a node has failed.
161 * The node that could not be connected
163 * The cause of the failure
165 private void fireNodeConnectionFailed(Node node, Throwable cause) {
166 for (NodeListener nodeListener : nodeListeners) {
167 nodeListener.nodeConnectionFailed(node, cause);
172 * Notifies all listeners that the given node was disconnected.
175 * The node that is now disconnected
177 * The exception that caused the disconnect, or <code>null</code>
178 * if there was no exception
180 private void fireNodeDisconnected(Node node, Throwable throwable) {
181 for (NodeListener nodeListener : nodeListeners) {
182 nodeListener.nodeDisconnected(node, throwable);
191 * Returns the directory in which the nodes are stored.
193 * @return The directory the nodes are stored in
195 public String getDirectory() {
200 * Checks whether the given node is already connected.
204 * @return <code>true</code> if the node is already connected,
205 * <code>false</code> otherwise
207 public boolean hasNode(Node node) {
208 return nodes.contains(node);
214 public Iterator<Node> iterator() {
215 return nodes.iterator();
225 * @throws IOException
226 * if an I/O error occurs loading the nodes
228 public void load() throws IOException {
229 File directoryFile = new File(directory);
230 File nodeFile = new File(directoryFile, "nodes.properties");
231 if (!nodeFile.exists() || !nodeFile.isFile() || !nodeFile.canRead()) {
234 Properties nodeProperties = new Properties();
235 InputStream nodeInputStream = null;
237 nodeInputStream = new FileInputStream(nodeFile);
238 nodeProperties.load(nodeInputStream);
240 Closer.close(nodeInputStream);
243 List<Node> loadedNodes = new ArrayList<Node>();
244 while (nodeProperties.containsKey("nodes." + ++nodeIndex + ".name")) {
245 String nodePrefix = "nodes." + nodeIndex;
246 String nodeId = nodeProperties.getProperty(nodePrefix + ".id");
247 if (nodeId == null) {
248 nodeId = Hex.toHex(IdGenerator.generateId());
250 String nodeName = nodeProperties.getProperty(nodePrefix + ".name");
251 if (!Verifier.verifyNodeName(nodeName)) {
252 logger.log(Level.WARNING, "invalid node name “" + nodeName + "”, skipping…");
255 String nodeHostname = nodeProperties.getProperty(nodePrefix + ".hostname");
256 if (!Verifier.verifyHostname(nodeHostname)) {
257 logger.log(Level.WARNING, "invalid host name “" + nodeHostname + "”");
258 /* not fatal, might be valid later on. */
260 String nodePortString = nodeProperties.getProperty(nodePrefix + ".port");
261 if (!Verifier.verifyPort(nodePortString)) {
262 logger.log(Level.WARNING, "invalid port number “" + nodePortString + "”, skipping…");
267 nodePort = Integer.valueOf(nodePortString);
268 } catch (NumberFormatException nfe1) {
269 /* shouldn't happen, port number was checked before. */
270 logger.log(Level.SEVERE, "invalid port number “" + nodePortString + "”, check failed! skipping…");
273 Node newNode = new Node();
274 newNode.setId(nodeId);
275 newNode.setName(nodeName);
276 newNode.setHostname(nodeHostname);
277 newNode.setPort(nodePort);
278 loadedNodes.add(newNode);
280 logger.fine("loaded " + loadedNodes.size() + " nodes from config");
281 synchronized (syncObject) {
283 for (Node node : loadedNodes) {
290 * Saves all configured nodes.
292 * @throws IOException
293 * if an I/O error occurs saving the nodes
295 public void save() throws IOException {
296 File directoryFile = new File(directory);
297 if (!directoryFile.exists()) {
298 if (!directoryFile.mkdirs()) {
299 throw new IOException("could not create directory: " + directory);
302 Properties nodeProperties = new Properties();
304 for (Node node : nodes) {
305 String nodePrefix = "nodes." + ++nodeIndex;
306 nodeProperties.setProperty(nodePrefix + ".id", node.getId());
307 nodeProperties.setProperty(nodePrefix + ".name", node.getName());
308 nodeProperties.setProperty(nodePrefix + ".hostname", node.getHostname());
309 nodeProperties.setProperty(nodePrefix + ".port", String.valueOf(node.getPort()));
311 File projectFile = new File(directoryFile, "nodes.properties");
312 OutputStream nodeOutputStream = null;
314 nodeOutputStream = new FileOutputStream(projectFile);
315 nodeProperties.store(nodeOutputStream, "jSite nodes");
317 Closer.close(nodeOutputStream);
322 * Adds the given node to this manager.
324 * @see #connect(Node)
326 * The node to connect to
327 * @return <code>true</code> if the node was added, <code>false</code>
328 * if the node was not added because it was already known
330 public boolean addNode(Node node) {
331 if (nodes.contains(node)) {
332 logger.warning("was told to add already known node: " + node);
335 node.addPropertyChangeListener(this);
336 HighLevelClient highLevelClient = new HighLevelClient(clientName);
338 idNodes.put(node.getId(), node);
339 clientNodes.put(highLevelClient, node);
340 nodeClients.put(node, highLevelClient);
341 highLevelClient.addHighLevelClientListener(this);
347 * Removes the given node from the node manager, disconnecting it if it is
348 * currently connected.
353 public void removeNode(Node node) {
354 synchronized (syncObject) {
355 if (!nodes.contains(node)) {
358 if (nodeClients.containsKey(node)) {
362 idNodes.remove(node.getId());
363 node.removePropertyChangeListener(this);
364 fireNodeRemoved(node);
369 * Tries to establish a connection with the given node.
372 * The node to connect to
374 public void connect(Node node) {
375 HighLevelClient highLevelClient;
376 highLevelClient = nodeClients.get(node);
377 if (highLevelClient == null) {
378 logger.warning("was told to connect to unknown node: " + node);
382 highLevelClient.connect(node.getHostname(), node.getPort());
383 } catch (UnknownHostException uhe1) {
384 fireNodeConnectionFailed(node, uhe1);
385 } catch (IOException ioe1) {
386 fireNodeConnectionFailed(node, ioe1);
391 * Disconnects the given node without removing it.
394 * The node to disconnect
396 public void disconnect(Node node) {
397 synchronized (syncObject) {
398 if (!nodes.contains(node)) {
401 HighLevelClient highLevelClient = nodeClients.get(node);
402 highLevelClient.disconnect();
407 * Returns a list of all nodes.
409 * @return A list of all nodes
411 public List<Node> getNodes() {
412 return Collections.unmodifiableList(nodes);
416 * Returns the high-level client for a given node.
419 * The node to get a high-level client for
420 * @return The high-level client for a node, or <code>null</code> if the
421 * node was disconnected or removed
423 public HighLevelClient getHighLevelClient(Node node) {
424 return nodeClients.get(node);
428 * Returns the node for a high-level client.
430 * @param highLevelClient
431 * The high-level client to get the node for
432 * @return The node for the high-level client, or <code>null</code> if the
433 * high-level client is not known
435 public Node getNode(HighLevelClient highLevelClient) {
436 return clientNodes.get(highLevelClient);
440 * Returns the node identified by the given ID.
444 * @return The node with the given ID, or <code>null</code> if no such
447 Node getNode(String id) {
448 return idNodes.get(id);
452 * Generates a new SSK key pair.
454 * @return An array with the private key at index <code>0</code> and the
455 * public key at index <code>1</code>
456 * @throws IOException
457 * if an I/O error occurs communicating with the node
458 * @throws JSiteException
459 * if there is a problem with the node
461 public String[] generateKeyPair() throws IOException, JSiteException {
462 if (nodes.isEmpty()) {
463 throw new NoNodeException("no node configured");
465 Node node = nodes.get(0);
466 HighLevelClient highLevelClient = nodeClients.get(node);
468 KeyGenerationResult keyGenerationResult = highLevelClient.generateKey().getResult();
469 return new String[] { keyGenerationResult.getInsertURI(), keyGenerationResult.getRequestURI() };
470 } catch (HighLevelException hle1) {
471 throw new BackendException(hle1);
472 } catch (InterruptedException e) {
483 // INTERFACE HighLevelClientListener
489 public void clientConnected(HighLevelClient highLevelClient) {
490 logger.log(Level.FINER, "clientConnected(c=" + highLevelClient + ")");
491 Node node = clientNodes.get(highLevelClient);
493 logger.log(Level.WARNING, "got event for unknown client");
496 fireNodeConnected(node);
502 public void clientDisconnected(HighLevelClient highLevelClient, Throwable throwable) {
503 logger.log(Level.FINER, "clientDisconnected(c=" + highLevelClient + ",t=" + throwable + ")");
504 synchronized (syncObject) {
505 Node node = clientNodes.get(highLevelClient);
507 logger.log(Level.WARNING, "got event for unknown client");
510 fireNodeDisconnected(node, throwable);
515 // INTERFACE PropertyChangeListener
521 public void propertyChange(PropertyChangeEvent propertyChangeEvent) {
522 Object eventSource = propertyChangeEvent.getSource();
523 if (eventSource instanceof Node) {
524 String propertyName = propertyChangeEvent.getPropertyName();
525 if ("hostname".equals(propertyName) || "port".equals(propertyName)) {
526 Node node = (Node) eventSource;
527 HighLevelClient highLevelClient = nodeClients.get(node);
528 if (highLevelClient == null) {
529 logger.log(Level.WARNING, "got property change event for unknown node: " + node);
532 if (highLevelClient.isConnected()) {
533 highLevelClient.disconnect();
535 highLevelClient.connect(node.getHostname(), node.getPort());
536 } catch (UnknownHostException uhe1) {
537 fireNodeConnectionFailed(node, uhe1);
538 } catch (IOException ioe1) {
539 fireNodeConnectionFailed(node, ioe1);