2 * jSite - Connection.java - Copyright © 2006–2012 David Roden
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation; either version 2 of the License, or
7 * (at your option) any later version.
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
14 * You should have received a copy of the GNU General Public License
15 * along with this program; if not, write to the Free Software
16 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
19 package de.todesbaum.util.freenet.fcp2;
21 import static java.util.logging.Level.FINE;
22 import static java.util.logging.Logger.getLogger;
25 import java.io.FileOutputStream;
26 import java.io.IOException;
27 import java.io.InputStream;
28 import java.io.OutputStream;
29 import java.io.OutputStreamWriter;
30 import java.io.Writer;
31 import java.net.Socket;
32 import java.nio.charset.Charset;
33 import java.util.ArrayList;
34 import java.util.List;
35 import java.util.logging.Logger;
37 import net.pterodactylus.util.io.Closer;
38 import net.pterodactylus.util.io.StreamCopier;
39 import net.pterodactylus.util.io.StreamCopier.ProgressListener;
40 import de.todesbaum.util.io.LineInputStream;
41 import de.todesbaum.util.io.TempFileInputStream;
44 * A physical connection to a Freenet node.
46 * @author David Roden <droden@gmail.com>
49 public class Connection {
51 private static final Logger logger = getLogger(Connection.class.getName());
53 /** The listeners that receive events from this connection. */
54 private List<ConnectionListener> connectionListeners = new ArrayList<ConnectionListener>();
56 /** The node this connection is connected to. */
57 private final Node node;
59 /** The name of this connection. */
60 private final String name;
62 /** The network socket of this connection. */
63 private Socket nodeSocket;
65 /** The input stream that reads from the socket. */
66 private InputStream nodeInputStream;
68 /** The output stream that writes to the socket. */
69 private OutputStream nodeOutputStream;
71 /** The thread that reads from the socket. */
72 private NodeReader nodeReader;
74 /** A writer for the output stream. */
75 private Writer nodeWriter;
77 /** The NodeHello message sent by the node on connect. */
78 protected Message nodeHello;
80 /** The temp directory to use. */
81 private String tempDirectory;
84 * Creates a new connection to the specified node with the specified name.
87 * The node to connect to
89 * The name of this connection
91 public Connection(Node node, String name) {
97 * Adds a listener that gets notified on connection events.
99 * @param connectionListener
100 * The listener to add
102 public void addConnectionListener(ConnectionListener connectionListener) {
103 connectionListeners.add(connectionListener);
107 * Removes a listener from the list of registered listeners. Only the first
108 * matching listener is removed.
110 * @param connectionListener
111 * The listener to remove
112 * @see List#remove(java.lang.Object)
114 public void removeConnectionListener(ConnectionListener connectionListener) {
115 connectionListeners.remove(connectionListener);
119 * Notifies listeners about a received message.
122 * The received message
124 protected void fireMessageReceived(Message message) {
125 for (ConnectionListener connectionListener : connectionListeners) {
126 connectionListener.messageReceived(this, message);
131 * Notifies listeners about the loss of the connection.
133 protected void fireConnectionTerminated() {
134 for (ConnectionListener connectionListener : connectionListeners) {
135 connectionListener.connectionTerminated(this);
140 * Returns the name of the connection.
142 * @return The name of the connection
144 public String getName() {
149 * Sets the temp directory to use for creation of temporary files.
151 * @param tempDirectory
152 * The temp directory to use, or {@code null} to use the default
155 public void setTempDirectory(String tempDirectory) {
156 this.tempDirectory = tempDirectory;
160 * Connects to the node.
162 * @return <code>true</code> if the connection succeeded and the node
163 * returned a NodeHello message
164 * @throws IOException
165 * if an I/O error occurs
166 * @see #getNodeHello()
168 public synchronized boolean connect() throws IOException {
170 nodeInputStream = null;
171 nodeOutputStream = null;
175 nodeSocket = new Socket(node.getHostname(), node.getPort());
176 nodeSocket.setReceiveBufferSize(65535);
177 nodeInputStream = nodeSocket.getInputStream();
178 nodeOutputStream = nodeSocket.getOutputStream();
179 nodeWriter = new OutputStreamWriter(nodeOutputStream, Charset.forName("UTF-8"));
180 nodeReader = new NodeReader(nodeInputStream);
181 Thread nodeReaderThread = new Thread(nodeReader);
182 nodeReaderThread.setDaemon(true);
183 nodeReaderThread.start();
184 ClientHello clientHello = new ClientHello();
185 clientHello.setName(name);
186 clientHello.setExpectedVersion("2.0");
187 execute(clientHello);
188 synchronized (this) {
191 } catch (InterruptedException e) {
194 return nodeHello != null;
195 } catch (IOException ioe1) {
202 * Returns whether this connection is still connected to the node.
204 * @return <code>true</code> if this connection is still valid,
205 * <code>false</code> otherwise
207 public boolean isConnected() {
208 return (nodeHello != null) && (nodeSocket != null) && (nodeSocket.isConnected());
212 * Returns the NodeHello message the node sent on connection.
214 * @return The NodeHello message of the node
216 public Message getNodeHello() {
221 * Disconnects from the node.
223 public void disconnect() {
224 Closer.close(nodeWriter);
226 Closer.close(nodeOutputStream);
227 nodeOutputStream = null;
228 Closer.close(nodeInputStream);
229 nodeInputStream = null;
230 nodeInputStream = null;
231 Closer.close(nodeSocket);
233 synchronized (this) {
236 logger.log(FINE, "Connection terminated.", new Exception());
237 fireConnectionTerminated();
241 * Executes the specified command.
244 * The command to execute
245 * @throws IllegalStateException
246 * if the connection is not connected
247 * @throws IOException
248 * if an I/O error occurs
250 public synchronized void execute(Command command) throws IllegalStateException, IOException {
251 execute(command, null);
255 * Executes the specified command.
258 * The command to execute
259 * @param progressListener
260 * A progress listener for a payload transfer
261 * @throws IllegalStateException
262 * if the connection is not connected
263 * @throws IOException
264 * if an I/O error occurs
266 public synchronized void execute(Command command, ProgressListener progressListener) throws IllegalStateException, IOException {
267 if (nodeSocket == null) {
268 throw new IllegalStateException("connection is not connected");
270 nodeWriter.write(command.getCommandName() + Command.LINEFEED);
271 command.write(nodeWriter);
272 nodeWriter.write("EndMessage" + Command.LINEFEED);
274 if (command.hasPayload()) {
275 InputStream payloadInputStream = null;
277 payloadInputStream = command.getPayload();
278 StreamCopier.copy(payloadInputStream, nodeOutputStream, progressListener, command.getPayloadLength());
280 Closer.close(payloadInputStream);
282 nodeOutputStream.flush();
287 * The reader thread for this connection. This is essentially a thread that
288 * reads lines from the node, creates messages from them and notifies
289 * listeners about the messages.
291 * @author David Roden <droden@gmail.com>
294 private class NodeReader implements Runnable {
296 /** The input stream to read from. */
297 @SuppressWarnings("hiding")
298 private InputStream nodeInputStream;
301 * Creates a new reader that reads from the specified input stream.
303 * @param nodeInputStream
304 * The input stream to read from
306 public NodeReader(InputStream nodeInputStream) {
307 this.nodeInputStream = nodeInputStream;
311 * Main loop of the reader. Lines are read and converted into
312 * {@link Message} objects.
314 @SuppressWarnings("synthetic-access")
316 LineInputStream nodeReader = null;
318 nodeReader = new LineInputStream(nodeInputStream);
320 Message message = null;
321 while (line != null) {
322 line = nodeReader.readLine();
323 // System.err.println("> " + line);
325 logger.fine("Connection was closed.");
328 if (message == null) {
329 message = new Message(line);
332 if ("Data".equals(line)) {
333 /* need to read message from stream now */
334 File tempFile = null;
336 tempFile = File.createTempFile("fcpv2", "data", (tempDirectory != null) ? new File(tempDirectory) : null);
337 tempFile.deleteOnExit();
338 FileOutputStream tempFileOutputStream = new FileOutputStream(tempFile);
339 long dataLength = Long.parseLong(message.get("DataLength"));
340 StreamCopier.copy(nodeInputStream, tempFileOutputStream, dataLength);
341 tempFileOutputStream.close();
342 message.setPayloadInputStream(new TempFileInputStream(tempFile));
343 } catch (IOException ioe1) {
344 ioe1.printStackTrace();
347 if ("Data".equals(line) || "EndMessage".equals(line)) {
348 if (message.getName().equals("NodeHello")) {
350 synchronized (Connection.this) {
351 Connection.this.notify();
354 fireMessageReceived(message);
359 int equalsPosition = line.indexOf('=');
360 if (equalsPosition > -1) {
361 String key = line.substring(0, equalsPosition).trim();
362 String value = line.substring(equalsPosition + 1).trim();
363 if (key.equals("Identifier")) {
364 message.setIdentifier(value);
366 message.put(key, value);
370 /* skip lines consisting of whitespace only */
371 if (line.trim().length() == 0) {
374 /* if we got here, some error occured! */
375 throw new IOException("Unexpected line: " + line);
377 } catch (IOException ioe1) {
378 logger.log(FINE, "Exception while reading from node.", ioe1);
380 if (nodeReader != null) {
383 } catch (IOException ioe1) {
386 if (nodeInputStream != null) {
388 nodeInputStream.close();
389 } catch (IOException ioe1) {
393 Connection.this.disconnect();