/* * jSite - Connection.java - Copyright © 2006–2012 David Roden * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ package de.todesbaum.util.freenet.fcp2; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.Writer; import java.net.Socket; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.List; import net.pterodactylus.util.io.Closer; import net.pterodactylus.util.io.StreamCopier; import net.pterodactylus.util.io.StreamCopier.ProgressListener; import de.todesbaum.util.io.LineInputStream; import de.todesbaum.util.io.TempFileInputStream; /** * A physical connection to a Freenet node. * * @author David Roden <droden@gmail.com> * @version $Id$ */ public class Connection { /** The listeners that receive events from this connection. */ private List<ConnectionListener> connectionListeners = new ArrayList<ConnectionListener>(); /** The node this connection is connected to. */ private final Node node; /** The name of this connection. */ private final String name; /** The network socket of this connection. */ private Socket nodeSocket; /** The input stream that reads from the socket. */ private InputStream nodeInputStream; /** The output stream that writes to the socket. */ private OutputStream nodeOutputStream; /** The thread that reads from the socket. */ private NodeReader nodeReader; /** A writer for the output stream. */ private Writer nodeWriter; /** The NodeHello message sent by the node on connect. */ protected Message nodeHello; /** The temp directory to use. */ private String tempDirectory; /** * Creates a new connection to the specified node with the specified name. * * @param node * The node to connect to * @param name * The name of this connection */ public Connection(Node node, String name) { this.node = node; this.name = name; } /** * Adds a listener that gets notified on connection events. * * @param connectionListener * The listener to add */ public void addConnectionListener(ConnectionListener connectionListener) { connectionListeners.add(connectionListener); } /** * Removes a listener from the list of registered listeners. Only the first * matching listener is removed. * * @param connectionListener * The listener to remove * @see List#remove(java.lang.Object) */ public void removeConnectionListener(ConnectionListener connectionListener) { connectionListeners.remove(connectionListener); } /** * Notifies listeners about a received message. * * @param message * The received message */ protected void fireMessageReceived(Message message) { for (ConnectionListener connectionListener : connectionListeners) { connectionListener.messageReceived(this, message); } } /** * Notifies listeners about the loss of the connection. */ protected void fireConnectionTerminated() { for (ConnectionListener connectionListener : connectionListeners) { connectionListener.connectionTerminated(this); } } /** * Returns the name of the connection. * * @return The name of the connection */ public String getName() { return name; } /** * Sets the temp directory to use for creation of temporary files. * * @param tempDirectory * The temp directory to use, or {@code null} to use the default * temp directory */ public void setTempDirectory(String tempDirectory) { this.tempDirectory = tempDirectory; } /** * Connects to the node. * * @return <code>true</code> if the connection succeeded and the node * returned a NodeHello message * @throws IOException * if an I/O error occurs * @see #getNodeHello() */ public synchronized boolean connect() throws IOException { nodeSocket = null; nodeInputStream = null; nodeOutputStream = null; nodeWriter = null; nodeReader = null; try { nodeSocket = new Socket(node.getHostname(), node.getPort()); nodeSocket.setReceiveBufferSize(65535); nodeInputStream = nodeSocket.getInputStream(); nodeOutputStream = nodeSocket.getOutputStream(); nodeWriter = new OutputStreamWriter(nodeOutputStream, Charset.forName("UTF-8")); nodeReader = new NodeReader(nodeInputStream); Thread nodeReaderThread = new Thread(nodeReader); nodeReaderThread.setDaemon(true); nodeReaderThread.start(); ClientHello clientHello = new ClientHello(); clientHello.setName(name); clientHello.setExpectedVersion("2.0"); execute(clientHello); synchronized (this) { try { wait(); } catch (InterruptedException e) { } } return nodeHello != null; } catch (IOException ioe1) { disconnect(); throw ioe1; } } /** * Returns whether this connection is still connected to the node. * * @return <code>true</code> if this connection is still valid, * <code>false</code> otherwise */ public boolean isConnected() { return (nodeHello != null) && (nodeSocket != null) && (nodeSocket.isConnected()); } /** * Returns the NodeHello message the node sent on connection. * * @return The NodeHello message of the node */ public Message getNodeHello() { return nodeHello; } /** * Disconnects from the node. */ public void disconnect() { Closer.close(nodeWriter); nodeWriter = null; Closer.close(nodeOutputStream); nodeOutputStream = null; Closer.close(nodeInputStream); nodeInputStream = null; nodeInputStream = null; Closer.close(nodeSocket); nodeSocket = null; synchronized (this) { notify(); } fireConnectionTerminated(); } /** * Executes the specified command. * * @param command * The command to execute * @throws IllegalStateException * if the connection is not connected * @throws IOException * if an I/O error occurs */ public synchronized void execute(Command command) throws IllegalStateException, IOException { execute(command, null); } /** * Executes the specified command. * * @param command * The command to execute * @param progressListener * A progress listener for a payload transfer * @throws IllegalStateException * if the connection is not connected * @throws IOException * if an I/O error occurs */ public synchronized void execute(Command command, ProgressListener progressListener) throws IllegalStateException, IOException { if (nodeSocket == null) { throw new IllegalStateException("connection is not connected"); } nodeWriter.write(command.getCommandName() + Command.LINEFEED); command.write(nodeWriter); nodeWriter.write("EndMessage" + Command.LINEFEED); nodeWriter.flush(); if (command.hasPayload()) { InputStream payloadInputStream = null; try { payloadInputStream = command.getPayload(); StreamCopier.copy(payloadInputStream, nodeOutputStream, progressListener, command.getPayloadLength()); } finally { Closer.close(payloadInputStream); } nodeOutputStream.flush(); } } /** * The reader thread for this connection. This is essentially a thread that * reads lines from the node, creates messages from them and notifies * listeners about the messages. * * @author David Roden <droden@gmail.com> * @version $Id$ */ private class NodeReader implements Runnable { /** The input stream to read from. */ @SuppressWarnings("hiding") private InputStream nodeInputStream; /** * Creates a new reader that reads from the specified input stream. * * @param nodeInputStream * The input stream to read from */ public NodeReader(InputStream nodeInputStream) { this.nodeInputStream = nodeInputStream; } /** * Main loop of the reader. Lines are read and converted into * {@link Message} objects. */ @SuppressWarnings("synthetic-access") public void run() { LineInputStream nodeReader = null; try { nodeReader = new LineInputStream(nodeInputStream); String line = ""; Message message = null; while (line != null) { line = nodeReader.readLine(); // System.err.println("> " + line); if (line == null) { break; } if (message == null) { message = new Message(line); continue; } if ("Data".equals(line)) { /* need to read message from stream now */ File tempFile = null; try { tempFile = File.createTempFile("fcpv2", "data", (tempDirectory != null) ? new File(tempDirectory) : null); tempFile.deleteOnExit(); FileOutputStream tempFileOutputStream = new FileOutputStream(tempFile); long dataLength = Long.parseLong(message.get("DataLength")); StreamCopier.copy(nodeInputStream, tempFileOutputStream, dataLength); tempFileOutputStream.close(); message.setPayloadInputStream(new TempFileInputStream(tempFile)); } catch (IOException ioe1) { ioe1.printStackTrace(); } } if ("Data".equals(line) || "EndMessage".equals(line)) { if (message.getName().equals("NodeHello")) { nodeHello = message; synchronized (Connection.this) { Connection.this.notify(); } } else { fireMessageReceived(message); } message = null; continue; } int equalsPosition = line.indexOf('='); if (equalsPosition > -1) { String key = line.substring(0, equalsPosition).trim(); String value = line.substring(equalsPosition + 1).trim(); if (key.equals("Identifier")) { message.setIdentifier(value); } else { message.put(key, value); } continue; } /* skip lines consisting of whitespace only */ if (line.trim().length() == 0) { continue; } /* if we got here, some error occured! */ throw new IOException("Unexpected line: " + line); } } catch (IOException ioe1) { // ioe1.printStackTrace(); } finally { if (nodeReader != null) { try { nodeReader.close(); } catch (IOException ioe1) { } } if (nodeInputStream != null) { try { nodeInputStream.close(); } catch (IOException ioe1) { } } } Connection.this.disconnect(); } } }