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.util.ArrayList;
29 import java.util.Collections;
30 import java.util.HashMap;
31 import java.util.HashSet;
32 import java.util.List;
34 import java.util.Properties;
36 import java.util.logging.Level;
37 import java.util.logging.Logger;
39 import net.pterodactylus.fcp.highlevel.HighLevelClient;
40 import net.pterodactylus.fcp.highlevel.HighLevelClientListener;
41 import net.pterodactylus.util.io.Closer;
42 import net.pterodactylus.util.logging.Logging;
47 * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
50 public class NodeManager implements HighLevelClientListener {
53 private static final Logger logger = Logging.getLogger(NodeManager.class.getName());
55 /** The FCP client name. */
56 private final String clientName;
58 /** The directory for the configuration. */
59 private final String directory;
61 /** Object used for synchronization. */
62 private final Object syncObject = new Object();
64 /** Node listeners. */
65 private List<NodeListener> nodeListeners = Collections.synchronizedList(new ArrayList<NodeListener>());
68 private List<Node> nodes = Collections.synchronizedList(new ArrayList<Node>());
70 /** All FCP connections. */
71 private Map<Node, HighLevelClient> nodeClients = Collections.synchronizedMap(new HashMap<Node, HighLevelClient>());
73 /** Keeps track of which connection is in use right now. */
74 private Set<HighLevelClient> usedConnections = Collections.synchronizedSet(new HashSet<HighLevelClient>());
76 /** Maps nodes to high-level clients. */
77 private Map<HighLevelClient, Node> clientNodes = Collections.synchronizedMap(new HashMap<HighLevelClient, Node>());
80 * Creates a new FCP collector.
83 * The name of the FCP client
85 * The directory in which to store the nodes
87 public NodeManager(String clientName, String directory) {
88 this.clientName = clientName;
89 this.directory = directory;
97 * Adds the given listener to the list of listeners.
100 * The listener to add
102 public void addNodeListener(NodeListener nodeListener) {
103 nodeListeners.add(nodeListener);
107 * Removes the given listener from the list of listeners.
109 * @param nodeListener
110 * The listener to remove
112 public void removeNodeListener(NodeListener nodeListener) {
113 nodeListeners.remove(nodeListener);
117 * Notifies all listeners that the given node was connected.
120 * The node that is now connected
122 private void fireNodeConnected(Node node) {
123 for (NodeListener nodeListener: nodeListeners) {
124 nodeListener.nodeConnected(node);
129 * Notifies all listeners that the given node was disconnected.
132 * The node that is now disconnected
134 * The exception that caused the disconnect, or <code>null</code>
135 * if there was no exception
137 private void fireNodeDisconnected(Node node, Throwable throwable) {
138 for (NodeListener nodeListener: nodeListeners) {
139 nodeListener.nodeDisconnected(node, throwable);
148 * Returns the directory in which the nodes are stored.
150 * @return The directory the nodes are stored in
152 public String getDirectory() {
157 * Checks whether the given node is already connected.
161 * @return <code>true</code> if the node is already connected,
162 * <code>false</code> otherwise
164 public boolean hasNode(Node node) {
165 return nodes.contains(node);
175 * @throws IOException
176 * if an I/O error occurs loading the nodes
178 public void load() throws IOException {
179 File directoryFile = new File(directory);
180 File nodeFile = new File(directoryFile, "nodes.properties");
181 if (!nodeFile.exists() || !nodeFile.isFile() || !nodeFile.canRead()) {
184 Properties nodeProperties = new Properties();
185 InputStream nodeInputStream = null;
187 nodeInputStream = new FileInputStream(nodeFile);
188 nodeProperties.load(nodeInputStream);
190 Closer.close(nodeInputStream);
193 List<Node> loadedNodes = new ArrayList<Node>();
194 while (nodeProperties.containsKey("nodes." + ++nodeIndex + ".name")) {
195 String nodePrefix = "nodes." + nodeIndex;
196 String nodeName = nodeProperties.getProperty(nodePrefix + ".name");
197 if (!Verifier.verifyNodeName(nodeName)) {
198 logger.log(Level.WARNING, "invalid node name “" + nodeName + "”, skipping…");
201 String nodeHostname = nodeProperties.getProperty(nodePrefix + ".hostname");
202 if (!Verifier.verifyHostname(nodeHostname)) {
203 logger.log(Level.WARNING, "invalid host name “" + nodeHostname + "”");
204 /* not fatal, might be valid later on. */
206 String nodePortString = nodeProperties.getProperty(nodePrefix + ".port");
207 if (!Verifier.verifyPort(nodePortString)) {
208 logger.log(Level.WARNING, "invalid port number “" + nodePortString + "”, skipping…");
213 nodePort = Integer.valueOf(nodePortString);
214 } catch (NumberFormatException nfe1) {
215 /* shouldn't happen, port number was checked before. */
216 logger.log(Level.SEVERE, "invalid port number “" + nodePortString + "”, check failed! skipping…");
219 Node newNode = new Node();
220 newNode.setName(nodeName);
221 newNode.setHostname(nodeHostname);
222 newNode.setPort(nodePort);
223 loadedNodes.add(newNode);
225 synchronized (syncObject) {
227 nodes.addAll(loadedNodes);
232 * Saves all configured nodes.
234 * @throws IOException
235 * if an I/O error occurs saving the nodes
237 public void save() throws IOException {
238 File directoryFile = new File(directory);
239 if (!directoryFile.exists()) {
240 if (!directoryFile.mkdirs()) {
241 throw new IOException("could not create directory: " + directory);
244 Properties nodeProperties = new Properties();
246 for (Node node: nodes) {
247 String nodePrefix = "nodes." + ++nodeIndex;
248 nodeProperties.setProperty(nodePrefix + ".name", node.getName());
249 nodeProperties.setProperty(nodePrefix + ".hostname", node.getHostname());
250 nodeProperties.setProperty(nodePrefix + ".port", String.valueOf(node.getPort()));
252 File projectFile = new File(directoryFile, "nodes.properties");
253 OutputStream nodeOutputStream = null;
255 nodeOutputStream = new FileOutputStream(projectFile);
256 nodeProperties.store(nodeOutputStream, "jSite nodes");
258 Closer.close(nodeOutputStream);
263 * Adds the given node to this manager.
265 * @see #connect(Node)
267 * The node to connect to
269 public void addNode(Node node) {
270 synchronized (syncObject) {
271 if (!nodes.contains(node)) {
278 * Removes the given node from the node manager, disconnecting it if it is
279 * currently connected.
284 public void removeNode(Node node) {
285 synchronized (syncObject) {
286 if (!nodes.contains(node)) {
289 if (nodeClients.containsKey(node)) {
296 * Tries to establish a connection with the given node.
299 * The node to connect to
301 public void connect(Node node) {
303 HighLevelClient highLevelClient = new HighLevelClient(clientName, node.getHostname(), node.getPort());
304 synchronized (syncObject) {
305 clientNodes.put(highLevelClient, node);
306 nodeClients.put(node, highLevelClient);
308 highLevelClient.addHighLevelClientListener(this);
309 highLevelClient.connect();
310 } catch (IOException ioe1) {
311 fireNodeDisconnected(node, ioe1);
316 * Disconnects the given node without removing it.
319 * The node to disconnect
321 public void disconnect(Node node) {
322 synchronized (syncObject) {
323 if (!nodes.contains(node)) {
326 HighLevelClient highLevelClient = nodeClients.get(node);
327 highLevelClient.disconnect();
332 * Returns a list of all nodes.
334 * @return A list of all nodes
336 public List<Node> getNodes() {
337 return Collections.unmodifiableList(nodes);
345 * Finds a currently unused high-level client, optionally waiting until a
346 * client is free and marking it used.
349 * <code>true</code> to wait for a free connection,
350 * <code>false</code> to return <code>null</code>
352 * <code>true</code> to mark the connection as used before
353 * returning it, <code>false</code> not to mark it
354 * @return An unused FCP connection, or <code>null</code> if no connection
357 @SuppressWarnings("unused")
358 private HighLevelClient findUnusedClient(boolean wait, boolean markAsUsed) {
359 synchronized (syncObject) {
360 HighLevelClient freeHighLevelClient = null;
361 while (freeHighLevelClient == null) {
362 for (HighLevelClient highLevelClient: nodeClients.values()) {
363 if (!usedConnections.contains(highLevelClient)) {
364 freeHighLevelClient = highLevelClient;
368 if (freeHighLevelClient != null) {
370 usedConnections.add(freeHighLevelClient);
372 return freeHighLevelClient;
378 /* we never get here, but the compiler doesn't realize. */
384 // INTERFACE HighLevelClientListener
390 public void clientConnected(HighLevelClient highLevelClient) {
391 logger.log(Level.FINER, "clientConnected(c=" + highLevelClient + ")");
392 Node node = clientNodes.get(highLevelClient);
394 logger.log(Level.WARNING, "got event for unknown client");
397 fireNodeConnected(node);
403 public void clientDisconnected(HighLevelClient highLevelClient, Throwable throwable) {
404 logger.log(Level.FINER, "clientDisconnected(c=" + highLevelClient + ",t=" + throwable +")");
405 synchronized (syncObject) {
406 Node node = clientNodes.remove(highLevelClient);
408 logger.log(Level.WARNING, "got event for unknown client");
411 nodeClients.remove(node);
412 fireNodeDisconnected(node, throwable);