add more status notifications to core listener
[jSite2.git] / src / net / pterodactylus / jsite / core / NodeManager.java
1 /*
2  * jSite2 - FcpCollector.java -
3  * Copyright © 2008 David Roden
4  *
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.
9  *
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.
14  *
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.
18  */
19
20 package net.pterodactylus.jsite.core;
21
22 import java.io.File;
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.List;
34 import java.util.Map;
35 import java.util.Properties;
36 import java.util.Set;
37 import java.util.logging.Level;
38 import java.util.logging.Logger;
39
40 import net.pterodactylus.fcp.highlevel.ConnectResult;
41 import net.pterodactylus.fcp.highlevel.HighLevelCallback;
42 import net.pterodactylus.fcp.highlevel.HighLevelClient;
43 import net.pterodactylus.util.io.Closer;
44
45 /**
46  * TODO
47  *
48  * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
49  * @version $Id$
50  */
51 public class NodeManager {
52
53         /** Logger. */
54         private static final Logger logger = Logger.getLogger(NodeManager.class.getName());
55
56         /** The FCP client name. */
57         private final String clientName;
58
59         /** The directory for the configuration. */
60         private final String directory;
61
62         /** Object used for synchronization. */
63         private final Object syncObject = new Object();
64
65         /** All nodes. */
66         private List<Node> nodes = Collections.synchronizedList(new ArrayList<Node>());
67
68         /** All FCP connections. */
69         private Map<Node, HighLevelClient> nodeConnections = Collections.synchronizedMap(new HashMap<Node, HighLevelClient>());
70
71         /** Keeps track of which connection is in use right now. */
72         private Set<HighLevelClient> usedConnections = Collections.synchronizedSet(new HashSet<HighLevelClient>());
73
74         /** Maps nodes to high-level clients. */
75         private Map<HighLevelClient, Node> clientNodes = Collections.synchronizedMap(new HashMap<HighLevelClient, Node>());
76
77         /**
78          * Creates a new FCP collector.
79          *
80          * @param clientName
81          *            The name of the FCP client
82          * @param directory
83          *            The directory in which to store the nodes
84          */
85         public NodeManager(String clientName, String directory) {
86                 this.clientName = clientName;
87                 this.directory = directory;
88         }
89
90         //
91         // ACCESSORS
92         //
93
94         /**
95          * Returns the directory in which the nodes are stored.
96          *
97          * @return The directory the nodes are stored in
98          */
99         public String getDirectory() {
100                 return directory;
101         }
102
103         /**
104          * Checks whether the given node is already connected.
105          *
106          * @param node
107          *            The node to check
108          * @return <code>true</code> if the node is already connected,
109          *         <code>false</code> otherwise
110          */
111         public boolean hasNode(Node node) {
112                 return nodes.contains(node);
113         }
114
115         //
116         // ACTIONS
117         //
118
119         /**
120          * Loads nodes.
121          *
122          * @throws IOException
123          *             if an I/O error occurs loading the nodes
124          */
125         public void load() throws IOException {
126                 File directoryFile = new File(directory);
127                 File nodeFile = new File(directoryFile, "nodes.properties");
128                 if (!nodeFile.exists() || !nodeFile.isFile() || !nodeFile.canRead()) {
129                         return;
130                 }
131                 Properties nodeProperties = new Properties();
132                 InputStream nodeInputStream = null;
133                 try {
134                         nodeInputStream = new FileInputStream(nodeFile);
135                         nodeProperties.load(nodeInputStream);
136                 } finally {
137                         Closer.close(nodeInputStream);
138                 }
139                 int nodeIndex = -1;
140                 List<Node> loadedNodes = new ArrayList<Node>();
141                 while (nodeProperties.containsKey("nodes." + ++nodeIndex + ".name")) {
142                         String nodePrefix = "nodes." + nodeIndex;
143                         String nodeName = nodeProperties.getProperty(nodePrefix + ".name");
144                         if (!Verifier.verifyNodeName(nodeName)) {
145                                 logger.log(Level.WARNING, "invalid node name “" + nodeName + "”, skipping…");
146                                 continue;
147                         }
148                         String nodeHostname = nodeProperties.getProperty(nodePrefix + ".hostname");
149                         if (!Verifier.verifyHostname(nodeHostname)) {
150                                 logger.log(Level.WARNING, "invalid host name “" + nodeHostname + "”");
151                                 /* not fatal, might be valid later on. */
152                         }
153                         String nodePortString = nodeProperties.getProperty(nodePrefix + ".port");
154                         if (!Verifier.verifyPort(nodePortString)) {
155                                 logger.log(Level.WARNING, "invalid port number “" + nodePortString + "”, skipping…");
156                                 continue;
157                         }
158                         int nodePort = -1;
159                         try {
160                                 nodePort = Integer.valueOf(nodePortString);
161                         } catch (NumberFormatException nfe1) {
162                                 /* shouldn't happen, port number was checked before. */
163                                 logger.log(Level.SEVERE, "invalid port number “" + nodePortString + "”, check failed! skipping…");
164                                 continue;
165                         }
166                         Node newNode = new Node();
167                         newNode.setName(nodeName);
168                         newNode.setHostname(nodeHostname);
169                         newNode.setPort(nodePort);
170                         loadedNodes.add(newNode);
171                 }
172                 synchronized (syncObject) {
173                         nodes.clear();
174                         nodes.addAll(loadedNodes);
175                 }
176         }
177
178         /**
179          * Saves all configured nodes.
180          *
181          * @throws IOException
182          *             if an I/O error occurs saving the nodes
183          */
184         public void save() throws IOException {
185                 File directoryFile = new File(directory);
186                 if (!directoryFile.exists()) {
187                         if (!directoryFile.mkdirs()) {
188                                 throw new IOException("could not create directory: " + directory);
189                         }
190                 }
191                 Properties nodeProperties = new Properties();
192                 int nodeIndex = -1;
193                 for (Node node: nodes) {
194                         String nodePrefix = "nodes." + ++nodeIndex;
195                         nodeProperties.setProperty(nodePrefix + ".name", node.getName());
196                         nodeProperties.setProperty(nodePrefix + ".hostname", node.getHostname());
197                         nodeProperties.setProperty(nodePrefix + ".port", String.valueOf(node.getPort()));
198                 }
199                 File projectFile = new File(directoryFile, "nodes.properties");
200                 OutputStream nodeOutputStream = null;
201                 try {
202                         nodeOutputStream = new FileOutputStream(projectFile);
203                         nodeProperties.store(nodeOutputStream, "jSite nodes");
204                 } finally {
205                         Closer.close(nodeOutputStream);
206                 }
207         }
208
209         /**
210          * Adds a connection to the given node. The connection is made instantly so
211          * this method may block. If the node can not be connected, it will not be
212          * added to the list of nodes.
213          *
214          * @param node
215          *            The node to connect to
216          * @return <code>true</code> if the connection to the node could be
217          *         established
218          * @throws UnknownHostException
219          *             if the hostname of the node can not be resolved
220          * @throws IOException
221          *             if an I/O error occurs connecting to the node
222          */
223         public boolean addNode(Node node) throws UnknownHostException, IOException {
224                 if (nodes.contains(node)) {
225                         return true;
226                 }
227                 HighLevelClient highLevelClient = new HighLevelClient(clientName, node.getHostname(), node.getPort());
228                 HighLevelCallback<ConnectResult> connectCallback = highLevelClient.connect();
229                 ConnectResult connectResult = null;
230                 while (connectResult == null) {
231                         try {
232                                 connectResult = connectCallback.getResult();
233                         } catch (InterruptedException e) {
234                                 /* ignore. */
235                         }
236                 }
237                 if (connectResult.isConnected()) {
238                         synchronized (syncObject) {
239                                 nodes.add(node);
240                                 nodeConnections.put(node, highLevelClient);
241                                 clientNodes.put(highLevelClient, node);
242                         }
243                 }
244                 return connectResult.isConnected();
245         }
246
247         /**
248          * Returns a list of all nodes.
249          *
250          * @return A list of all nodes
251          */
252         public List<Node> getNodes() {
253                 return new ArrayList<Node>(clientNodes.values());
254         }
255
256         //
257         // PRIVATE METHODS
258         //
259
260         /**
261          * Finds a currently unused high-level client, optionally waiting until a
262          * client is free and marking it used.
263          *
264          * @param wait
265          *            <code>true</code> to wait for a free connection,
266          *            <code>false</code> to return <code>null</code>
267          * @param markAsUsed
268          *            <code>true</code> to mark the connection as used before
269          *            returning it, <code>false</code> not to mark it
270          * @return An unused FCP connection, or <code>null</code> if no connection
271          *         could be found
272          */
273         @SuppressWarnings("unused")
274         private HighLevelClient findUnusedClient(boolean wait, boolean markAsUsed) {
275                 synchronized (syncObject) {
276                         HighLevelClient freeHighLevelClient = null;
277                         while (freeHighLevelClient == null) {
278                                 for (HighLevelClient highLevelClient: nodeConnections.values()) {
279                                         if (!usedConnections.contains(highLevelClient)) {
280                                                 freeHighLevelClient = highLevelClient;
281                                                 break;
282                                         }
283                                 }
284                                 if (freeHighLevelClient != null) {
285                                         if (markAsUsed) {
286                                                 usedConnections.add(freeHighLevelClient);
287                                         }
288                                         return freeHighLevelClient;
289                                 }
290                                 if (!wait) {
291                                         return null;
292                                 }
293                         }
294                         /* we never get here, but the compiler doesn't realize. */
295                         return null;
296                 }
297         }
298
299 }