From f8b1b38d714ac20b94867919582cce31ef7b807c Mon Sep 17 00:00:00 2001 From: =?utf8?q?David=20=E2=80=98Bombe=E2=80=99=20Roden?= Date: Mon, 17 Nov 2014 21:32:09 +0100 Subject: [PATCH] Extract connection interface. --- .../java/net/pterodactylus/irc/Connection.java | 522 +------------------- .../net/pterodactylus/irc/DefaultConnection.java | 527 +++++++++++++++++++++ .../irc/DefaultConnectionFactory.java | 2 +- .../net/pterodactylus/irc/NicknameChooser.java | 6 +- .../java/net/pterodactylus/xdcc/core/Core.java | 6 +- 5 files changed, 558 insertions(+), 505 deletions(-) create mode 100644 src/main/java/net/pterodactylus/irc/DefaultConnection.java diff --git a/src/main/java/net/pterodactylus/irc/Connection.java b/src/main/java/net/pterodactylus/irc/Connection.java index a14c739..580b4d7 100644 --- a/src/main/java/net/pterodactylus/irc/Connection.java +++ b/src/main/java/net/pterodactylus/irc/Connection.java @@ -1,175 +1,35 @@ -/* - * XdccDownloader - Connection.java - Copyright © 2013 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 3 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, see . - */ - package net.pterodactylus.irc; -import static com.google.common.base.Preconditions.checkState; -import static java.util.Arrays.asList; -import static java.util.concurrent.TimeUnit.SECONDS; - -import java.io.BufferedReader; -import java.io.Closeable; -import java.io.EOFException; import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.io.UnsupportedEncodingException; -import java.net.Socket; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; - -import javax.net.SocketFactory; - -import net.pterodactylus.irc.connection.ChannelNickHandler; -import net.pterodactylus.irc.connection.ChannelNotJoinedHandler; -import net.pterodactylus.irc.connection.ConnectionEstablishHandler; -import net.pterodactylus.irc.connection.CtcpHandler; -import net.pterodactylus.irc.connection.Handler; -import net.pterodactylus.irc.connection.MessageHandler; -import net.pterodactylus.irc.connection.MotdHandler; -import net.pterodactylus.irc.connection.PrefixHandler; -import net.pterodactylus.irc.connection.SimpleCommandHandler; -import net.pterodactylus.irc.event.ChannelJoined; -import net.pterodactylus.irc.event.ChannelLeft; -import net.pterodactylus.irc.event.ChannelTopic; -import net.pterodactylus.irc.event.ClientQuit; -import net.pterodactylus.irc.event.ConnectionClosed; -import net.pterodactylus.irc.event.ConnectionEstablished; -import net.pterodactylus.irc.event.ConnectionFailed; -import net.pterodactylus.irc.event.KickedFromChannel; -import net.pterodactylus.irc.event.NicknameChanged; -import net.pterodactylus.irc.event.NicknameInUseReceived; -import net.pterodactylus.irc.event.NoNicknameGivenReceived; -import net.pterodactylus.irc.event.ReplyReceived; -import net.pterodactylus.irc.event.UnknownReplyReceived; -import net.pterodactylus.irc.util.RandomNickname; -import net.pterodactylus.xdcc.util.io.BandwidthCountingInputStream; -import net.pterodactylus.xdcc.util.io.BandwidthCountingOutputStream; - -import com.google.common.base.Optional; -import com.google.common.eventbus.EventBus; -import com.google.common.eventbus.Subscribe; -import com.google.common.io.Closeables; -import com.google.common.util.concurrent.AbstractExecutionThreadService; -import com.google.common.util.concurrent.Service; -import org.apache.log4j.Logger; /** * A connection to an IRC server. * * @author David ‘Bombe’ Roden */ -public class Connection extends AbstractExecutionThreadService implements Service { - - /* The logger. */ - private static final Logger logger = Logger.getLogger(Connection.class.getName()); - - /** The event bus. */ - private final EventBus eventBus; - - /** The socket factory. */ - private final SocketFactory socketFactory; - - /** The hostname to connect to. */ - private final String hostname; - - /** The port to connect to. */ - private final int port; - - /** The nickname chooser. */ - private NicknameChooser nicknameChooser = new NicknameChooser() { - - @Override - public String getNickname() { - return RandomNickname.get(); - } - }; - - /** The nickname. */ - private String nickname = null; - - /** The username. */ - private Optional username = Optional.absent(); - - /** The real name. */ - private Optional realName = Optional.absent(); - - /** The optional password for the connection. */ - private Optional password = Optional.absent(); - - /** The connection handler. */ - private ConnectionHandler connectionHandler; - - /** Whether the connection has already been established. */ - private final AtomicBoolean established = new AtomicBoolean(); - - /** - * Creates a new connection. - * - * @param eventBus - * The event bus - * @param socketFactory - * The socket factory - * @param hostname - * The hostname of the IRC server - * @param port - * The port number of the IRC server - */ - public Connection(EventBus eventBus, SocketFactory socketFactory, String hostname, int port) { - this.eventBus = eventBus; - this.socketFactory = socketFactory; - this.hostname = hostname; - this.port = port; - } - - // - // ACCESSORS - // +public interface Connection { /** * Returns the hostname of the remote end of the connection. * * @return The remote’s hostname */ - public String hostname() { - return hostname; - } + String hostname(); /** * Returns the port number of the remote end of the connection. * * @return The remote’s port number */ - public int port() { - return port; - } + int port(); /** * Returns whether this connection has already been established. * * @return {@code true} as long as this connection is established, {@code - * false} otherwise + * false} otherwise */ - public boolean established() { - return established.get(); - } + boolean established(); /** * Returns the nickname that is currently in use by this connection. The @@ -177,26 +37,7 @@ public class Connection extends AbstractExecutionThreadService implements Servic * * @return The current nickname */ - public String nickname() { - return nickname; - } - - // - // MUTATORS - // - - /** - * Sets the nickname chooser. The nickname chooser is only used during the - * creation of the connection. - * - * @param nicknameChooser - * The nickname chooser - * @return This connection - */ - public Connection nicknameChooser(NicknameChooser nicknameChooser) { - this.nicknameChooser = nicknameChooser; - return this; - } + String nickname(); /** * Sets the username to use. @@ -205,10 +46,7 @@ public class Connection extends AbstractExecutionThreadService implements Servic * The username to use * @return This connection */ - public Connection username(String username) { - this.username = Optional.fromNullable(username); - return this; - } + Connection username(String username); /** * Sets the real name to use. @@ -217,10 +55,7 @@ public class Connection extends AbstractExecutionThreadService implements Servic * The real name to use * @return This connection */ - public Connection realName(String realName) { - this.realName = Optional.fromNullable(realName); - return this; - } + Connection realName(String realName); /** * Sets the optional password for the connection. @@ -229,32 +64,21 @@ public class Connection extends AbstractExecutionThreadService implements Servic * The password for the connection * @return This connection */ - public Connection password(String password) { - this.password = Optional.fromNullable(password); - return this; - } - - // - // ACTIONS - // + Connection password(String password); /** * Returns the current rate of the connection’s incoming side. * * @return The current input rate (in bytes per second) */ - public long getInputRate() { - return (connectionHandler != null) ? connectionHandler.getInputRate() : 0; - } + long getInputRate(); /** * Returns the current rate of the connection’s outgoing side. * * @return The current output rate (in bytes per second) */ - public long getOutputRate() { - return (connectionHandler != null) ? connectionHandler.getOutputRate() : 0; - } + long getOutputRate(); /** * Checks whether the given source is the client represented by this @@ -263,11 +87,9 @@ public class Connection extends AbstractExecutionThreadService implements Servic * @param source * The source to check * @return {@code true} if this connection represents the given source, {@code - * false} otherwise + * false} otherwise */ - public boolean isSource(Source source) { - return source.nick().isPresent() && source.nick().get().equals(nickname); - } + boolean isSource(Source source); /** * Joins the given channel. @@ -277,9 +99,7 @@ public class Connection extends AbstractExecutionThreadService implements Servic * @throws IOException * if an I/O error occurs */ - public void joinChannel(final String channel) throws IOException { - connectionHandler.sendCommand("JOIN", channel); - } + void joinChannel(String channel) throws IOException; /** * Sends a message to the given recipient, which may be a channel or another @@ -292,9 +112,7 @@ public class Connection extends AbstractExecutionThreadService implements Servic * @throws IOException * if an I/O error occurs */ - public void sendMessage(String recipient, String message) throws IOException { - connectionHandler.sendCommand("PRIVMSG", recipient, message); - } + void sendMessage(String recipient, String message) throws IOException; /** * Sends a DCC RESUME request to the given recipient. @@ -310,314 +128,20 @@ public class Connection extends AbstractExecutionThreadService implements Servic * @throws IOException * if an I/O error occurs */ - public void sendDccResume(String recipient, String filename, int port, long position) throws IOException { - connectionHandler.sendCommand("PRIVMSG", recipient, String.format("\u0001DCC RESUME %s %d %d\u0001", filename, port, position)); - } + void sendDccResume(String recipient, String filename, int port, long position) + throws IOException; /** - * Closes this connection. - * - * @throws IOException - * if an I/O error occurs + * Opens the connection and starts processing messages from the other side. */ - public void close() throws IOException { - if (connectionHandler != null) { - connectionHandler.close(); - } - } - - // - // ABSTRACTEXECUTIONTHREADSERVICE METHODS - // - - @Override - protected void startUp() throws IllegalStateException { - checkState(username.isPresent(), "username must be set"); - checkState(realName.isPresent(), "realName must be set"); - } - - @Override - protected void run() { - - /* connect to remote socket. */ - try { - Socket socket = socketFactory.createSocket(hostname, port); - socket.setSoTimeout((int) TimeUnit.MINUTES.toMillis(3)); - connectionHandler = new ConnectionHandler(socket.getInputStream(), socket.getOutputStream()); - - /* register connection. */ - if (password.isPresent()) { - connectionHandler.sendCommand("PASSWORD", password.get()); - } - connectionHandler.sendCommand("USER", username.get(), "8", "*", realName.get()); - nickname = nicknameChooser.getNickname(); - connectionHandler.sendCommand("NICK", nickname); - - } catch (IOException ioe1) { - eventBus.post(new ConnectionFailed(this, ioe1)); - return; - } - - eventBus.register(this); - /* now read replies and react. */ - try { - /* some status variables. */ - boolean connected = true; - - PrefixHandler prefixHandler = new PrefixHandler(); - List handlers = asList( - new MessageHandler(eventBus, this, prefixHandler), - new CtcpHandler(eventBus, this), - new ChannelNickHandler(eventBus, this, prefixHandler), - new SimpleCommandHandler(eventBus) - .addCommand("431", - (s, p) -> new NoNicknameGivenReceived( - this)) - .addCommand("NICK", - (s, p) -> new NicknameChanged(this, - s.get(), p.get(0))) - .addCommand("JOIN", - (s, p) -> new ChannelJoined(this, - p.get(0), s.get())) - .addCommand("332", - (s, p) -> new ChannelTopic(this, p.get(1), - p.get(2))) - .addCommand("PART", - (s, p) -> new ChannelLeft(this, p.get(0), - s.get(), getOptional(p, 1))) - .addCommand("QUIT", - (s, p) -> new ClientQuit(this, s.get(), - p.get(0))) - .addCommand("KICK", - (s, p) -> new KickedFromChannel(this, - p.get(0), s.get(), p.get(1), - getOptional(p, 2))), - new MotdHandler(eventBus, this), - new ChannelNotJoinedHandler(eventBus, this), - new ConnectionEstablishHandler(eventBus, this), - prefixHandler - ); - - while (connected) { - Reply reply = connectionHandler.readReply(); - eventBus.post(new ReplyReceived(this, reply)); - logger.trace(String.format("<< %s", reply)); - String command = reply.command(); - List parameters = reply.parameters(); - - for (Handler handler : handlers) { - if (handler.willHandle(reply)) { - handler.handleReply(reply); - break; - } - } - - /* 43x replies are for nick change errors. */ - if (command.equals("433")) { - if (!established.get()) { - nickname = nicknameChooser.getNickname(); - connectionHandler.sendCommand("NICK", nickname); - } else { - eventBus.post(new NicknameInUseReceived(this, reply)); - } - - - /* basic connection housekeeping. */ - } else if (command.equalsIgnoreCase("PING")) { - connectionHandler.sendCommand("PONG", getOptional(parameters, 0), getOptional(parameters, 1)); - - /* okay, everything else. */ - } else { - eventBus.post(new UnknownReplyReceived(this, reply)); - } - } - eventBus.post(new ConnectionClosed(this)); - } catch (IOException ioe1) { - logger.warn("I/O error", ioe1); - eventBus.post(new ConnectionClosed(this, ioe1)); - } catch (RuntimeException re1) { - logger.error("Runtime error", re1); - eventBus.post(new ConnectionClosed(this, re1)); - } finally { - established.set(false); - eventBus.unregister(this); - logger.info("Closing Connection."); - try { - Closeables.close(connectionHandler, true); - } catch (IOException ioe1) { - /* will not be thrown. */ - } - } - - } - - @Subscribe - public void connectionEstablished(ConnectionEstablished connectionEstablished) { - if (connectionEstablished.connection() == this) { - established.set(true); - } - } - - // - // PRIVATE METHODS - // + void open(); /** - * Returns an item from the list, or {@link Optional#absent()} if the list is - * shorter than required for the given index. + * Closes this connection. * - * @param list - * The list to get an item from - * @param index - * The index of the item - * @param - * The type of the list items - * @return This list item wrapped in an {@link Optional}, or {@link - * Optional#absent()} if the list is not long enough + * @throws IOException + * if an I/O error occurs */ - private static Optional getOptional(List list, int index) { - if (index < list.size()) { - return Optional.fromNullable(list.get(index)); - } - return Optional.absent(); - } - - /** Handles input and output for the connection. */ - private class ConnectionHandler implements Closeable { - - /** The output stream of the connection. */ - private final BandwidthCountingOutputStream outputStream; - - /** The input stream. */ - private final BandwidthCountingInputStream inputStream; - - /** The input stream of the connection. */ - private final BufferedReader inputStreamReader; - - /** - * Creates a new connection handler for the given input stream and output - * stream. - * - * @param inputStream - * The input stream of the connection - * @param outputStream - * The output stream of the connection - * @throws UnsupportedEncodingException - * if the encoding (currently “UTF-8”) is not valid - */ - private ConnectionHandler(InputStream inputStream, OutputStream outputStream) throws UnsupportedEncodingException { - this.outputStream = new BandwidthCountingOutputStream(outputStream, 5, SECONDS); - this.inputStream = new BandwidthCountingInputStream(inputStream, 5, SECONDS); - inputStreamReader = new BufferedReader(new InputStreamReader(this.inputStream, "UTF-8")); - } - - // - // ACTIONS - // - - /** - * Returns the current rate of the connection’s incoming side. - * - * @return The current input rate (in bytes per second) - */ - public long getInputRate() { - return inputStream.getCurrentRate(); - } - - /** - * Returns the current rate of the connection’s outgoing side. - * - * @return The current output rate (in bytes per second) - */ - public long getOutputRate() { - return outputStream.getCurrentRate(); - } - - /** - * Sends a command with the given parameters, skipping all {@link - * Optional#absent()} optionals. - * - * @param command - * The command to send - * @param parameters - * The parameters - * @throws IOException - * if an I/O error occurs - */ - public void sendCommand(String command, Optional... parameters) throws IOException { - List setParameters = new ArrayList(); - for (Optional maybeSetParameter : parameters) { - if (maybeSetParameter.isPresent()) { - setParameters.add(maybeSetParameter.get()); - } - } - sendCommand(command, setParameters.toArray(new String[setParameters.size()])); - } - - /** - * Sends a command with the given parameters. - * - * @param command - * The command to send - * @param parameters - * The parameters of the command - * @throws IOException - * if an I/O error occurs - * @throws IllegalArgumentException - * if any parameter but that last contains a space character - */ - public void sendCommand(String command, String... parameters) throws IOException, IllegalArgumentException { - StringBuilder commandBuilder = new StringBuilder(); - - commandBuilder.append(command); - for (int parameterIndex = 0; parameterIndex < parameters.length; ++parameterIndex) { - String parameter = parameters[parameterIndex]; - /* space is only allowed in the last parameter. */ - commandBuilder.append(' '); - if (parameter.contains(" ")) { - if (parameterIndex == (parameters.length - 1)) { - commandBuilder.append(':'); - } else { - throw new IllegalArgumentException(String.format("parameter “%s” must not contain space!", parameter)); - } - } - commandBuilder.append(parameter); - } - - logger.trace(String.format(">> %s", commandBuilder)); - outputStream.write((commandBuilder.toString() + "\r\n").getBytes("UTF-8")); - outputStream.flush(); - } - - /** - * Reads a line of reply from the connection. - * - * @return The reply - * @throws IOException - * if an I/O error occurs - * @throws EOFException - * if EOF was reached - */ - public Reply readReply() throws IOException, EOFException { - String line = inputStreamReader.readLine(); - if (line == null) { - throw new EOFException(); - } - - return Reply.parseLine(line); - } - - // - // CLOSEABLE METHODS - // - - @Override - public void close() throws IOException { - Closeables.close(outputStream, true); - Closeables.close(inputStreamReader, true); - Closeables.close(inputStream, true); - } - - } + void close() throws IOException; } diff --git a/src/main/java/net/pterodactylus/irc/DefaultConnection.java b/src/main/java/net/pterodactylus/irc/DefaultConnection.java new file mode 100644 index 0000000..cca6aed --- /dev/null +++ b/src/main/java/net/pterodactylus/irc/DefaultConnection.java @@ -0,0 +1,527 @@ +/* + * XdccDownloader - Connection.java - Copyright © 2013 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 3 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, see . + */ + +package net.pterodactylus.irc; + +import static com.google.common.base.Preconditions.checkState; +import static java.util.Arrays.asList; +import static java.util.concurrent.TimeUnit.SECONDS; + +import java.io.BufferedReader; +import java.io.Closeable; +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.net.Socket; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +import javax.net.SocketFactory; + +import net.pterodactylus.irc.connection.ChannelNickHandler; +import net.pterodactylus.irc.connection.ChannelNotJoinedHandler; +import net.pterodactylus.irc.connection.ConnectionEstablishHandler; +import net.pterodactylus.irc.connection.CtcpHandler; +import net.pterodactylus.irc.connection.Handler; +import net.pterodactylus.irc.connection.MessageHandler; +import net.pterodactylus.irc.connection.MotdHandler; +import net.pterodactylus.irc.connection.PrefixHandler; +import net.pterodactylus.irc.connection.SimpleCommandHandler; +import net.pterodactylus.irc.event.ChannelJoined; +import net.pterodactylus.irc.event.ChannelLeft; +import net.pterodactylus.irc.event.ChannelTopic; +import net.pterodactylus.irc.event.ClientQuit; +import net.pterodactylus.irc.event.ConnectionClosed; +import net.pterodactylus.irc.event.ConnectionEstablished; +import net.pterodactylus.irc.event.ConnectionFailed; +import net.pterodactylus.irc.event.KickedFromChannel; +import net.pterodactylus.irc.event.NicknameChanged; +import net.pterodactylus.irc.event.NicknameInUseReceived; +import net.pterodactylus.irc.event.NoNicknameGivenReceived; +import net.pterodactylus.irc.event.ReplyReceived; +import net.pterodactylus.irc.event.UnknownReplyReceived; +import net.pterodactylus.irc.util.RandomNickname; +import net.pterodactylus.xdcc.util.io.BandwidthCountingInputStream; +import net.pterodactylus.xdcc.util.io.BandwidthCountingOutputStream; + +import com.google.common.base.Optional; +import com.google.common.eventbus.EventBus; +import com.google.common.eventbus.Subscribe; +import com.google.common.io.Closeables; +import com.google.common.util.concurrent.AbstractExecutionThreadService; +import com.google.common.util.concurrent.Service; +import org.apache.log4j.Logger; + +/** + * A connection to an IRC server. + * + * @author David ‘Bombe’ Roden + */ +public class DefaultConnection extends AbstractExecutionThreadService implements Service, + Connection { + + /* The logger. */ + private static final Logger logger = Logger.getLogger(DefaultConnection.class.getName()); + + /** The event bus. */ + private final EventBus eventBus; + + /** The socket factory. */ + private final SocketFactory socketFactory; + + /** The hostname to connect to. */ + private final String hostname; + + /** The port to connect to. */ + private final int port; + + /** The nickname chooser. */ + private NicknameChooser nicknameChooser = new NicknameChooser() { + + @Override + public String getNickname() { + return RandomNickname.get(); + } + }; + + /** The nickname. */ + private String nickname = null; + + /** The username. */ + private Optional username = Optional.absent(); + + /** The real name. */ + private Optional realName = Optional.absent(); + + /** The optional password for the connection. */ + private Optional password = Optional.absent(); + + /** The connection handler. */ + private ConnectionHandler connectionHandler; + + /** Whether the connection has already been established. */ + private final AtomicBoolean established = new AtomicBoolean(); + + /** + * Creates a new connection. + * + * @param eventBus + * The event bus + * @param socketFactory + * The socket factory + * @param hostname + * The hostname of the IRC server + * @param port + * The port number of the IRC server + */ + public DefaultConnection(EventBus eventBus, SocketFactory socketFactory, String hostname, + int port) { + this.eventBus = eventBus; + this.socketFactory = socketFactory; + this.hostname = hostname; + this.port = port; + } + + // + // ACCESSORS + // + + @Override + public String hostname() { + return hostname; + } + + @Override + public int port() { + return port; + } + + @Override + public boolean established() { + return established.get(); + } + + @Override + public String nickname() { + return nickname; + } + + @Override + public Connection username(String username) { + this.username = Optional.fromNullable(username); + return this; + } + + @Override + public Connection realName(String realName) { + this.realName = Optional.fromNullable(realName); + return this; + } + + @Override + public Connection password(String password) { + this.password = Optional.fromNullable(password); + return this; + } + + // + // ACTIONS + // + + @Override + public long getInputRate() { + return (connectionHandler != null) ? connectionHandler.getInputRate() : 0; + } + + @Override + public long getOutputRate() { + return (connectionHandler != null) ? connectionHandler.getOutputRate() : 0; + } + + @Override + public boolean isSource(Source source) { + return source.nick().isPresent() && source.nick().get().equals(nickname); + } + + @Override + public void joinChannel(final String channel) throws IOException { + connectionHandler.sendCommand("JOIN", channel); + } + + @Override + public void sendMessage(String recipient, String message) throws IOException { + connectionHandler.sendCommand("PRIVMSG", recipient, message); + } + + @Override + public void sendDccResume(String recipient, String filename, int port, long position) throws IOException { + connectionHandler.sendCommand("PRIVMSG", recipient, + String.format("\u0001DCC RESUME %s %d %d\u0001", filename, port, position)); + } + + @Override + public void open() { + start(); + } + + @Override + public void close() throws IOException { + if (connectionHandler != null) { + connectionHandler.close(); + } + } + + // + // ABSTRACTEXECUTIONTHREADSERVICE METHODS + // + + @Override + protected void startUp() throws IllegalStateException { + checkState(username.isPresent(), "username must be set"); + checkState(realName.isPresent(), "realName must be set"); + } + + @Override + protected void run() { + + /* connect to remote socket. */ + try { + Socket socket = socketFactory.createSocket(hostname, port); + socket.setSoTimeout((int) TimeUnit.MINUTES.toMillis(3)); + connectionHandler = new ConnectionHandler(socket.getInputStream(), socket.getOutputStream()); + + /* register connection. */ + if (password.isPresent()) { + connectionHandler.sendCommand("PASSWORD", password.get()); + } + connectionHandler.sendCommand("USER", username.get(), "8", "*", realName.get()); + nickname = nicknameChooser.getNickname(); + connectionHandler.sendCommand("NICK", nickname); + + } catch (IOException ioe1) { + eventBus.post(new ConnectionFailed(this, ioe1)); + return; + } + + eventBus.register(this); + /* now read replies and react. */ + try { + /* some status variables. */ + boolean connected = true; + + PrefixHandler prefixHandler = new PrefixHandler(); + List handlers = asList( + new MessageHandler(eventBus, this, prefixHandler), + new CtcpHandler(eventBus, this), + new ChannelNickHandler(eventBus, this, prefixHandler), + new SimpleCommandHandler(eventBus) + .addCommand("431", + (s, p) -> new NoNicknameGivenReceived( + this)) + .addCommand("NICK", + (s, p) -> new NicknameChanged(this, + s.get(), p.get(0))) + .addCommand("JOIN", + (s, p) -> new ChannelJoined(this, + p.get(0), s.get())) + .addCommand("332", + (s, p) -> new ChannelTopic(this, p.get(1), + p.get(2))) + .addCommand("PART", + (s, p) -> new ChannelLeft(this, p.get(0), + s.get(), getOptional(p, 1))) + .addCommand("QUIT", + (s, p) -> new ClientQuit(this, s.get(), + p.get(0))) + .addCommand("KICK", + (s, p) -> new KickedFromChannel(this, + p.get(0), s.get(), p.get(1), + getOptional(p, 2))), + new MotdHandler(eventBus, this), + new ChannelNotJoinedHandler(eventBus, this), + new ConnectionEstablishHandler(eventBus, this), + prefixHandler + ); + + while (connected) { + Reply reply = connectionHandler.readReply(); + eventBus.post(new ReplyReceived(this, reply)); + logger.trace(String.format("<< %s", reply)); + String command = reply.command(); + List parameters = reply.parameters(); + + for (Handler handler : handlers) { + if (handler.willHandle(reply)) { + handler.handleReply(reply); + break; + } + } + + /* 43x replies are for nick change errors. */ + if (command.equals("433")) { + if (!established.get()) { + nickname = nicknameChooser.getNickname(); + connectionHandler.sendCommand("NICK", nickname); + } else { + eventBus.post(new NicknameInUseReceived(this, reply)); + } + + + /* basic connection housekeeping. */ + } else if (command.equalsIgnoreCase("PING")) { + connectionHandler.sendCommand("PONG", getOptional(parameters, 0), getOptional(parameters, 1)); + + /* okay, everything else. */ + } else { + eventBus.post(new UnknownReplyReceived(this, reply)); + } + } + eventBus.post(new ConnectionClosed(this)); + } catch (IOException ioe1) { + logger.warn("I/O error", ioe1); + eventBus.post(new ConnectionClosed(this, ioe1)); + } catch (RuntimeException re1) { + logger.error("Runtime error", re1); + eventBus.post(new ConnectionClosed(this, re1)); + } finally { + established.set(false); + eventBus.unregister(this); + logger.info("Closing Connection."); + try { + Closeables.close(connectionHandler, true); + } catch (IOException ioe1) { + /* will not be thrown. */ + } + } + + } + + @Subscribe + public void connectionEstablished(ConnectionEstablished connectionEstablished) { + if (connectionEstablished.connection() == this) { + established.set(true); + } + } + + // + // PRIVATE METHODS + // + + /** + * Returns an item from the list, or {@link Optional#absent()} if the list is + * shorter than required for the given index. + * + * @param list + * The list to get an item from + * @param index + * The index of the item + * @param + * The type of the list items + * @return This list item wrapped in an {@link Optional}, or {@link + * Optional#absent()} if the list is not long enough + */ + private static Optional getOptional(List list, int index) { + if (index < list.size()) { + return Optional.fromNullable(list.get(index)); + } + return Optional.absent(); + } + + /** Handles input and output for the connection. */ + private class ConnectionHandler implements Closeable { + + /** The output stream of the connection. */ + private final BandwidthCountingOutputStream outputStream; + + /** The input stream. */ + private final BandwidthCountingInputStream inputStream; + + /** The input stream of the connection. */ + private final BufferedReader inputStreamReader; + + /** + * Creates a new connection handler for the given input stream and output + * stream. + * + * @param inputStream + * The input stream of the connection + * @param outputStream + * The output stream of the connection + * @throws UnsupportedEncodingException + * if the encoding (currently “UTF-8”) is not valid + */ + private ConnectionHandler(InputStream inputStream, OutputStream outputStream) throws UnsupportedEncodingException { + this.outputStream = new BandwidthCountingOutputStream(outputStream, 5, SECONDS); + this.inputStream = new BandwidthCountingInputStream(inputStream, 5, SECONDS); + inputStreamReader = new BufferedReader(new InputStreamReader(this.inputStream, "UTF-8")); + } + + // + // ACTIONS + // + + /** + * Returns the current rate of the connection’s incoming side. + * + * @return The current input rate (in bytes per second) + */ + public long getInputRate() { + return inputStream.getCurrentRate(); + } + + /** + * Returns the current rate of the connection’s outgoing side. + * + * @return The current output rate (in bytes per second) + */ + public long getOutputRate() { + return outputStream.getCurrentRate(); + } + + /** + * Sends a command with the given parameters, skipping all {@link + * Optional#absent()} optionals. + * + * @param command + * The command to send + * @param parameters + * The parameters + * @throws IOException + * if an I/O error occurs + */ + public void sendCommand(String command, Optional... parameters) throws IOException { + List setParameters = new ArrayList(); + for (Optional maybeSetParameter : parameters) { + if (maybeSetParameter.isPresent()) { + setParameters.add(maybeSetParameter.get()); + } + } + sendCommand(command, setParameters.toArray(new String[setParameters.size()])); + } + + /** + * Sends a command with the given parameters. + * + * @param command + * The command to send + * @param parameters + * The parameters of the command + * @throws IOException + * if an I/O error occurs + * @throws IllegalArgumentException + * if any parameter but that last contains a space character + */ + public void sendCommand(String command, String... parameters) throws IOException, IllegalArgumentException { + StringBuilder commandBuilder = new StringBuilder(); + + commandBuilder.append(command); + for (int parameterIndex = 0; parameterIndex < parameters.length; ++parameterIndex) { + String parameter = parameters[parameterIndex]; + /* space is only allowed in the last parameter. */ + commandBuilder.append(' '); + if (parameter.contains(" ")) { + if (parameterIndex == (parameters.length - 1)) { + commandBuilder.append(':'); + } else { + throw new IllegalArgumentException(String.format("parameter “%s” must not contain space!", parameter)); + } + } + commandBuilder.append(parameter); + } + + logger.trace(String.format(">> %s", commandBuilder)); + outputStream.write((commandBuilder.toString() + "\r\n").getBytes("UTF-8")); + outputStream.flush(); + } + + /** + * Reads a line of reply from the connection. + * + * @return The reply + * @throws IOException + * if an I/O error occurs + * @throws EOFException + * if EOF was reached + */ + public Reply readReply() throws IOException, EOFException { + String line = inputStreamReader.readLine(); + if (line == null) { + throw new EOFException(); + } + + return Reply.parseLine(line); + } + + // + // CLOSEABLE METHODS + // + + @Override + public void close() throws IOException { + Closeables.close(outputStream, true); + Closeables.close(inputStreamReader, true); + Closeables.close(inputStream, true); + } + + } + +} diff --git a/src/main/java/net/pterodactylus/irc/DefaultConnectionFactory.java b/src/main/java/net/pterodactylus/irc/DefaultConnectionFactory.java index 6a37394..96e7e62 100644 --- a/src/main/java/net/pterodactylus/irc/DefaultConnectionFactory.java +++ b/src/main/java/net/pterodactylus/irc/DefaultConnectionFactory.java @@ -19,7 +19,7 @@ public class DefaultConnectionFactory implements ConnectionFactory { @Override public Connection createConnection(String hostname, int port) { - return new Connection(eventBus, SocketFactory.getDefault(), hostname, port); + return new DefaultConnection(eventBus, SocketFactory.getDefault(), hostname, port); } } diff --git a/src/main/java/net/pterodactylus/irc/NicknameChooser.java b/src/main/java/net/pterodactylus/irc/NicknameChooser.java index 9a9357a..7731805 100644 --- a/src/main/java/net/pterodactylus/irc/NicknameChooser.java +++ b/src/main/java/net/pterodactylus/irc/NicknameChooser.java @@ -18,8 +18,8 @@ package net.pterodactylus.irc; /** - * A nickname choose is used to get a new nickname. It is used by the {@link - * Connection} to retrieve the initial nickname of the connection, and it’s also + * A nickname chooser is used to get a new nickname. It is used by the {@link + * DefaultConnection} to retrieve the initial nickname of the connection, and it’s also * used in case a nick change results in a “nickname already in use” error. * * @author David ‘Bombe’ Roden @@ -31,6 +31,6 @@ public interface NicknameChooser { * * @return A nickname */ - public String getNickname(); + String getNickname(); } diff --git a/src/main/java/net/pterodactylus/xdcc/core/Core.java b/src/main/java/net/pterodactylus/xdcc/core/Core.java index ab9804a..1b45c6a 100644 --- a/src/main/java/net/pterodactylus/xdcc/core/Core.java +++ b/src/main/java/net/pterodactylus/xdcc/core/Core.java @@ -41,6 +41,7 @@ import java.util.stream.Collectors; import net.pterodactylus.irc.Connection; import net.pterodactylus.irc.ConnectionFactory; import net.pterodactylus.irc.DccReceiver; +import net.pterodactylus.irc.DefaultConnection; import net.pterodactylus.irc.event.ChannelJoined; import net.pterodactylus.irc.event.ChannelLeft; import net.pterodactylus.irc.event.ChannelMessageReceived; @@ -430,10 +431,11 @@ public class Core extends AbstractExecutionThreadService { return; } Server server = servers.get((int) (Math.random() * servers.size())); - Connection connection = connectionFactory.createConnection(server.hostname(), server.unencryptedPorts().iterator().next()); + Connection connection = connectionFactory.createConnection(server.hostname(), + server.unencryptedPorts().iterator().next()); connection.username(RandomNickname.get()).realName(RandomNickname.get()); networkConnections.put(network, connection); - connection.start(); + connection.open(); } } -- 2.7.4