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;
23 import java.io.FileInputStream;
24 import java.io.FileOutputStream;
25 import java.io.IOException;
26 import java.io.InputStream;
27 import java.io.OutputStream;
28 import java.net.UnknownHostException;
29 import java.util.ArrayList;
30 import java.util.Collections;
31 import java.util.HashMap;
32 import java.util.HashSet;
33 import java.util.Iterator;
34 import java.util.List;
36 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.util.io.Closer;
44 import net.pterodactylus.util.logging.Logging;
49 * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
52 public class NodeManager implements Iterable<Node>, HighLevelClientListener {
55 private static final Logger logger = Logging.getLogger(NodeManager.class.getName());
57 /** The FCP client name. */
58 private final String clientName;
60 /** The directory for the configuration. */
61 private final String directory;
63 /** Object used for synchronization. */
64 private final Object syncObject = new Object();
66 /** Node listeners. */
67 private List<NodeListener> nodeListeners = Collections.synchronizedList(new ArrayList<NodeListener>());
70 private List<Node> nodes = Collections.synchronizedList(new ArrayList<Node>());
72 /** All FCP connections. */
73 private Map<Node, HighLevelClient> nodeClients = Collections.synchronizedMap(new HashMap<Node, HighLevelClient>());
75 /** Keeps track of which connection is in use right now. */
76 private Set<HighLevelClient> usedConnections = Collections.synchronizedSet(new HashSet<HighLevelClient>());
78 /** Maps nodes to high-level clients. */
79 private Map<HighLevelClient, Node> clientNodes = Collections.synchronizedMap(new HashMap<HighLevelClient, Node>());
82 * Creates a new FCP collector.
85 * The name of the FCP client
87 * The directory in which to store the nodes
89 public NodeManager(String clientName, String directory) {
90 this.clientName = clientName;
91 this.directory = directory;
99 * Adds the given listener to the list of listeners.
101 * @param nodeListener
102 * The listener to add
104 public void addNodeListener(NodeListener nodeListener) {
105 nodeListeners.add(nodeListener);
109 * Removes the given listener from the list of listeners.
111 * @param nodeListener
112 * The listener to remove
114 public void removeNodeListener(NodeListener nodeListener) {
115 nodeListeners.remove(nodeListener);
119 * Notifies all listeners that a node was added.
122 * The node that was added.
124 private void fireNodeAdded(Node node) {
125 for (NodeListener nodeListener: nodeListeners) {
126 nodeListener.nodeAdded(node);
131 * Notifies all listeners that a node was removed.
134 * The node that was removed
136 private void fireNodeRemoved(Node node) {
137 for (NodeListener nodeListener: nodeListeners) {
138 nodeListener.nodeRemoved(node);
143 * Notifies all listeners that the given node was connected.
146 * The node that is now connected
148 private void fireNodeConnected(Node node) {
149 for (NodeListener nodeListener: nodeListeners) {
150 nodeListener.nodeConnected(node);
155 * Notifies all listeners that the given node was disconnected.
158 * The node that is now disconnected
160 * The exception that caused the disconnect, or <code>null</code>
161 * if there was no exception
163 private void fireNodeDisconnected(Node node, Throwable throwable) {
164 for (NodeListener nodeListener: nodeListeners) {
165 nodeListener.nodeDisconnected(node, throwable);
174 * Returns the directory in which the nodes are stored.
176 * @return The directory the nodes are stored in
178 public String getDirectory() {
183 * Checks whether the given node is already connected.
187 * @return <code>true</code> if the node is already connected,
188 * <code>false</code> otherwise
190 public boolean hasNode(Node node) {
191 return nodes.contains(node);
197 public Iterator<Node> iterator() {
198 return nodes.iterator();
208 * @throws IOException
209 * if an I/O error occurs loading the nodes
211 public void load() throws IOException {
212 File directoryFile = new File(directory);
213 File nodeFile = new File(directoryFile, "nodes.properties");
214 if (!nodeFile.exists() || !nodeFile.isFile() || !nodeFile.canRead()) {
217 Properties nodeProperties = new Properties();
218 InputStream nodeInputStream = null;
220 nodeInputStream = new FileInputStream(nodeFile);
221 nodeProperties.load(nodeInputStream);
223 Closer.close(nodeInputStream);
226 List<Node> loadedNodes = new ArrayList<Node>();
227 while (nodeProperties.containsKey("nodes." + ++nodeIndex + ".name")) {
228 String nodePrefix = "nodes." + nodeIndex;
229 String nodeName = nodeProperties.getProperty(nodePrefix + ".name");
230 if (!Verifier.verifyNodeName(nodeName)) {
231 logger.log(Level.WARNING, "invalid node name “" + nodeName + "”, skipping…");
234 String nodeHostname = nodeProperties.getProperty(nodePrefix + ".hostname");
235 if (!Verifier.verifyHostname(nodeHostname)) {
236 logger.log(Level.WARNING, "invalid host name “" + nodeHostname + "”");
237 /* not fatal, might be valid later on. */
239 String nodePortString = nodeProperties.getProperty(nodePrefix + ".port");
240 if (!Verifier.verifyPort(nodePortString)) {
241 logger.log(Level.WARNING, "invalid port number “" + nodePortString + "”, skipping…");
246 nodePort = Integer.valueOf(nodePortString);
247 } catch (NumberFormatException nfe1) {
248 /* shouldn't happen, port number was checked before. */
249 logger.log(Level.SEVERE, "invalid port number “" + nodePortString + "”, check failed! skipping…");
252 Node newNode = new Node();
253 newNode.setName(nodeName);
254 newNode.setHostname(nodeHostname);
255 newNode.setPort(nodePort);
256 loadedNodes.add(newNode);
258 logger.fine("loaded " + loadedNodes.size() + " nodes from config");
259 synchronized (syncObject) {
261 for (Node node: nodes) {
268 * Saves all configured nodes.
270 * @throws IOException
271 * if an I/O error occurs saving the nodes
273 public void save() throws IOException {
274 File directoryFile = new File(directory);
275 if (!directoryFile.exists()) {
276 if (!directoryFile.mkdirs()) {
277 throw new IOException("could not create directory: " + directory);
280 Properties nodeProperties = new Properties();
282 for (Node node: nodes) {
283 String nodePrefix = "nodes." + ++nodeIndex;
284 nodeProperties.setProperty(nodePrefix + ".name", node.getName());
285 nodeProperties.setProperty(nodePrefix + ".hostname", node.getHostname());
286 nodeProperties.setProperty(nodePrefix + ".port", String.valueOf(node.getPort()));
288 File projectFile = new File(directoryFile, "nodes.properties");
289 OutputStream nodeOutputStream = null;
291 nodeOutputStream = new FileOutputStream(projectFile);
292 nodeProperties.store(nodeOutputStream, "jSite nodes");
294 Closer.close(nodeOutputStream);
299 * Adds the given node to this manager.
301 * @see #connect(Node)
303 * The node to connect to
304 * @throws UnknownHostException
305 * if the hostname of the node can not be resolved
307 public void addNode(Node node) throws UnknownHostException {
308 synchronized (syncObject) {
309 if (!nodes.contains(node)) {
310 HighLevelClient highLevelClient= new HighLevelClient(clientName, node.getHostname(), node.getPort());
312 clientNodes.put(highLevelClient, node);
313 nodeClients.put(node, highLevelClient);
314 highLevelClient.addHighLevelClientListener(this);
321 * Removes the given node from the node manager, disconnecting it if it is
322 * currently connected.
327 public void removeNode(Node node) {
328 synchronized (syncObject) {
329 if (!nodes.contains(node)) {
332 if (nodeClients.containsKey(node)) {
335 fireNodeRemoved(node);
340 * Tries to establish a connection with the given node.
343 * The node to connect to
345 public void connect(Node node) {
346 HighLevelClient highLevelClient;
347 synchronized (syncObject) {
348 highLevelClient = nodeClients.get(node);
351 highLevelClient.connect();
352 } catch (IOException ioe1) {
353 fireNodeDisconnected(node, ioe1);
358 * Disconnects the given node without removing it.
361 * The node to disconnect
363 public void disconnect(Node node) {
364 synchronized (syncObject) {
365 if (!nodes.contains(node)) {
368 HighLevelClient highLevelClient = nodeClients.get(node);
369 highLevelClient.disconnect();
374 * Returns a list of all nodes.
376 * @return A list of all nodes
378 public List<Node> getNodes() {
379 return Collections.unmodifiableList(nodes);
383 * “Borrows” a high-level client for the given node. A borrowed client
384 * <strong>has</strong> to be returned to the node manager using
385 * {@link #returnHighLevelClient(HighLevelClient)} when it is no longer in
386 * use, i.e. after a message has been sent! This method will block until a
387 * high-level client for the given node is available.
390 * The node to get a high-level client for
391 * @return The high-level client for a node, or <code>null</code> if the
392 * node was disconnected or removed
394 public HighLevelClient borrowHighLevelClient(Node node) {
395 synchronized (syncObject) {
396 if (!nodeClients.containsKey(node)) {
399 HighLevelClient highLevelClient = nodeClients.get(node);
400 while (nodeClients.containsKey(node) && usedConnections.contains(highLevelClient)) {
403 } catch (InterruptedException ie1) {
404 /* ignore. TODO - check. */
407 if (!nodeClients.containsKey(node)) {
410 usedConnections.add(highLevelClient);
411 return highLevelClient;
416 * Returns a borrowed high-level client.
418 * @see #borrowHighLevelClient(Node)
419 * @param highLevelClient
420 * The high-level client to return
422 public void returnHighLevelClient(HighLevelClient highLevelClient) {
423 synchronized (syncObject) {
424 if (!clientNodes.containsKey(highLevelClient)) {
427 usedConnections.remove(highLevelClient);
428 syncObject.notifyAll();
437 * Finds a currently unused high-level client, optionally waiting until a
438 * client is free and marking it used.
441 * <code>true</code> to wait for a free connection,
442 * <code>false</code> to return <code>null</code>
444 * <code>true</code> to mark the connection as used before
445 * returning it, <code>false</code> not to mark it
446 * @return An unused FCP connection, or <code>null</code> if no connection
449 @SuppressWarnings("unused")
450 private HighLevelClient findUnusedClient(boolean wait, boolean markAsUsed) {
451 synchronized (syncObject) {
452 HighLevelClient freeHighLevelClient = null;
453 while (freeHighLevelClient == null) {
454 for (HighLevelClient highLevelClient: nodeClients.values()) {
455 if (!usedConnections.contains(highLevelClient)) {
456 freeHighLevelClient = highLevelClient;
460 if (freeHighLevelClient != null) {
462 usedConnections.add(freeHighLevelClient);
464 return freeHighLevelClient;
471 } catch (InterruptedException e) {
472 /* ignore, just re-check. */
475 /* we never get here, but the compiler doesn't realize. */
481 // INTERFACE HighLevelClientListener
487 public void clientConnected(HighLevelClient highLevelClient) {
488 logger.log(Level.FINER, "clientConnected(c=" + highLevelClient + ")");
489 Node node = clientNodes.get(highLevelClient);
491 logger.log(Level.WARNING, "got event for unknown client");
494 fireNodeConnected(node);
500 public void clientDisconnected(HighLevelClient highLevelClient, Throwable throwable) {
501 logger.log(Level.FINER, "clientDisconnected(c=" + highLevelClient + ",t=" + throwable + ")");
502 synchronized (syncObject) {
503 Node node = clientNodes.remove(highLevelClient);
505 logger.log(Level.WARNING, "got event for unknown client");
508 nodeClients.remove(node);
509 usedConnections.remove(highLevelClient);
510 fireNodeDisconnected(node, throwable);