Split command reader into separate commands.
authorDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Fri, 6 Sep 2013 23:29:51 +0000 (01:29 +0200)
committerDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Fri, 6 Sep 2013 23:36:04 +0000 (01:36 +0200)
src/main/java/net/pterodactylus/xdcc/ui/stdin/AbortDownloadCommand.java [new file with mode: 0644]
src/main/java/net/pterodactylus/xdcc/ui/stdin/Command.java [new file with mode: 0644]
src/main/java/net/pterodactylus/xdcc/ui/stdin/CommandReader.java
src/main/java/net/pterodactylus/xdcc/ui/stdin/DisconnectCommand.java [new file with mode: 0644]
src/main/java/net/pterodactylus/xdcc/ui/stdin/DownloadCommand.java [new file with mode: 0644]
src/main/java/net/pterodactylus/xdcc/ui/stdin/ListConnectionsCommand.java [new file with mode: 0644]
src/main/java/net/pterodactylus/xdcc/ui/stdin/ListDownloadsCommand.java [new file with mode: 0644]
src/main/java/net/pterodactylus/xdcc/ui/stdin/Result.java [new file with mode: 0644]
src/main/java/net/pterodactylus/xdcc/ui/stdin/SearchCommand.java [new file with mode: 0644]
src/main/java/net/pterodactylus/xdcc/ui/stdin/State.java [new file with mode: 0644]
src/main/java/net/pterodactylus/xdcc/ui/stdin/StatsCommand.java [new file with mode: 0644]

diff --git a/src/main/java/net/pterodactylus/xdcc/ui/stdin/AbortDownloadCommand.java b/src/main/java/net/pterodactylus/xdcc/ui/stdin/AbortDownloadCommand.java
new file mode 100644 (file)
index 0000000..c09232b
--- /dev/null
@@ -0,0 +1,74 @@
+/*
+ * XdccDownloader - AbortDownloadCommand.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 <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.xdcc.ui.stdin;
+
+import java.io.IOException;
+import java.io.Writer;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+
+import net.pterodactylus.xdcc.core.Core;
+
+import com.google.common.primitives.Ints;
+
+/**
+ * Command that will abort a running download.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ * @see State#getLastDownloads()
+ */
+public class AbortDownloadCommand implements Command {
+
+       /** The core to operate on. */
+       private final Core core;
+
+       /**
+        * Creates a new abort download command.
+        *
+        * @param core
+        *              The core to operate on
+        */
+       public AbortDownloadCommand(Core core) {
+               this.core = core;
+       }
+
+       //
+       // COMMAND METHODS
+       //
+
+       @Override
+       public String getName() {
+               return "abort";
+       }
+
+       @Override
+       public Collection<String> getAliases() {
+               return Arrays.asList("cancel");
+       }
+
+       @Override
+       public State execute(State state, List<String> parameters, Writer outputWriter) throws IOException {
+               Integer index = Ints.tryParse(parameters.get(0));
+               if ((index != null) && (index < state.getLastDownloads().size())) {
+                       core.cancelDownload(state.getLastDownloads().get(index).bot(), state.getLastDownloads().get(index).pack());
+               }
+               return state;
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/xdcc/ui/stdin/Command.java b/src/main/java/net/pterodactylus/xdcc/ui/stdin/Command.java
new file mode 100644 (file)
index 0000000..87d0188
--- /dev/null
@@ -0,0 +1,82 @@
+/*
+ * XdccDownloader - Command.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 <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.xdcc.ui.stdin;
+
+import java.io.IOException;
+import java.io.Writer;
+import java.util.Collection;
+import java.util.List;
+
+import com.google.common.base.Function;
+
+/**
+ * A command is executed by the {@link CommandReader}. It receives the current
+ * state of the command reader and can return a changed state, depending on the
+ * given parameters.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public interface Command {
+
+       /** Converts a command into its name. */
+       public static final Function<Command, String> TO_NAME = new Function<Command, String>() {
+               @Override
+               public String apply(Command command) {
+                       return command.getName();
+               }
+       };
+
+       /** Converts a command into its aliases. */
+       public static final Function<Command, Collection<String>> TO_ALIASES = new Function<Command, Collection<String>>() {
+               @Override
+               public Collection<String> apply(Command command) {
+                       return command.getAliases();
+               }
+       };
+
+       /**
+        * Returns the name of this command.
+        *
+        * @return The name of this command
+        */
+       public String getName();
+
+       /**
+        * Returns possible aliases for this command.
+        *
+        * @return Possible aliases for this command
+        */
+       public Collection<String> getAliases();
+
+       /**
+        * Executes this command.
+        *
+        * @param state
+        *              The current state of the command reader
+        * @param parameters
+        *              The parameters given on the command line
+        * @param outputWriter
+        *              The output writer to write the output of the command to
+        * @return The new state of the command reader (which may be the given state if
+        *         the command does not need to modify the state)
+        * @throws IOException
+        *              if an I/O error occurs
+        */
+       public State execute(State state, List<String> parameters, Writer outputWriter) throws IOException;
+
+}
index 2a6ec83..46b22ba 100644 (file)
 
 package net.pterodactylus.xdcc.ui.stdin;
 
-import static com.google.common.collect.Lists.newArrayList;
-import static net.pterodactylus.xdcc.data.Download.BY_NAME;
-import static net.pterodactylus.xdcc.data.Download.BY_RUNNING;
+import static com.google.common.collect.FluentIterable.from;
+import static java.util.Arrays.asList;
+import static net.pterodactylus.xdcc.ui.stdin.Command.TO_NAME;
 
 import java.io.BufferedReader;
 import java.io.IOException;
 import java.io.Reader;
 import java.io.Writer;
-import java.util.Arrays;
 import java.util.Collection;
-import java.util.Collections;
-import java.util.Comparator;
 import java.util.List;
-import java.util.Set;
-import java.util.regex.Pattern;
 
-import net.pterodactylus.irc.Connection;
-import net.pterodactylus.irc.DccReceiver;
 import net.pterodactylus.irc.util.MessageCleaner;
 import net.pterodactylus.xdcc.core.Core;
 import net.pterodactylus.xdcc.core.event.DownloadFailed;
@@ -42,18 +35,12 @@ import net.pterodactylus.xdcc.core.event.DownloadFinished;
 import net.pterodactylus.xdcc.core.event.DownloadStarted;
 import net.pterodactylus.xdcc.core.event.GenericMessage;
 import net.pterodactylus.xdcc.core.event.MessageReceived;
-import net.pterodactylus.xdcc.data.Bot;
 import net.pterodactylus.xdcc.data.Download;
-import net.pterodactylus.xdcc.data.Pack;
 
-import com.google.common.base.Predicate;
-import com.google.common.collect.ComparisonChain;
-import com.google.common.collect.FluentIterable;
-import com.google.common.collect.Lists;
-import com.google.common.collect.Ordering;
-import com.google.common.collect.Sets;
+import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
 import com.google.common.eventbus.Subscribe;
-import com.google.common.primitives.Ints;
 import com.google.common.util.concurrent.AbstractExecutionThreadService;
 
 /**
@@ -63,8 +50,8 @@ import com.google.common.util.concurrent.AbstractExecutionThreadService;
  */
 public class CommandReader extends AbstractExecutionThreadService {
 
-       /** The core being controlled. */
-       private final Core core;
+       /** The commands to process. */
+       private final Collection<Command> commands;
 
        /** The reader to read commands from. */
        private final BufferedReader reader;
@@ -83,9 +70,19 @@ public class CommandReader extends AbstractExecutionThreadService {
         *              The write to write results to
         */
        public CommandReader(Core core, Reader reader, Writer writer) {
-               this.core = core;
                this.reader = new BufferedReader(reader);
                this.writer = writer;
+
+               /* initialize commands. */
+               ImmutableList.Builder<Command> commandBuilder = ImmutableList.builder();
+               commandBuilder.add(new ListDownloadsCommand(core));
+               commandBuilder.add(new SearchCommand(core));
+               commandBuilder.add(new DownloadCommand(core));
+               commandBuilder.add(new StatsCommand(core));
+               commandBuilder.add(new ListConnectionsCommand(core));
+               commandBuilder.add(new AbortDownloadCommand(core));
+               commandBuilder.add(new DisconnectCommand(core));
+               commands = commandBuilder.build();
        }
 
        //
@@ -96,108 +93,23 @@ public class CommandReader extends AbstractExecutionThreadService {
        protected void run() throws Exception {
                String lastLine = "";
                String line;
-               State state = new State();
+               net.pterodactylus.xdcc.ui.stdin.State state = new net.pterodactylus.xdcc.ui.stdin.State();
                while ((line = reader.readLine()) != null) {
+                       line = line.trim();
                        if (line.equals("")) {
                                line = lastLine;
                        }
                        String[] words = line.split(" +");
-                       if (words[0].equalsIgnoreCase("search")) {
-                               List<Result> lastResult = newArrayList();
-                               for (Bot bot : newArrayList(core.bots())) {
-                                       for (Pack pack : newArrayList(bot)) {
-                                               boolean found = true;
-                                               for (int wordIndex = 1; wordIndex < words.length; ++wordIndex) {
-                                                       if (words[wordIndex].startsWith("-") && pack.name().toLowerCase().contains(words[wordIndex].toLowerCase().substring(1))) {
-                                                               found = false;
-                                                               break;
-                                                       }
-                                                       if (!words[wordIndex].startsWith("-") && !pack.name().toLowerCase().contains(words[wordIndex].toLowerCase())) {
-                                                               found = false;
-                                                               break;
-                                                       }
-                                               }
-                                               if (found) {
-                                                       lastResult.add(new Result(bot, pack));
-                                               }
-                                       }
-                               }
-                               Collections.sort(lastResult);
-                               int counter = 0;
-                               for (Result result : lastResult) {
-                                       writeLine(String.format("[%d] %s (%s) from %s (#%s) on %s", counter++, result.pack().name(), result.pack().size(), result.bot().name(), result.pack().id(), result.bot().network().name()));
-                               }
-                               writeLine("End of Search.");
-                               state = state.setLastResults(lastResult);
-                       } else if (words[0].equalsIgnoreCase("dcc")) {
-                               int counter = 0;
-                               List<Download> downloads = newArrayList(FluentIterable.from(core.downloads()).toSortedList(Ordering.from(BY_NAME).compound(BY_RUNNING)));
-                               for (Download download : downloads) {
-                                       DccReceiver dccReceiver = download.dccReceiver();
-                                       if (dccReceiver == null) {
-                                               /* download has not even started. */
-                                               writer.write(String.format("[%d] %s requested from %s (not started yet)\n", counter++, download.pack().name(), download.bot().name()));
-                                               continue;
-                                       }
-                                       writer.write(String.format("[%d] %s from %s (%s, ", counter++, dccReceiver.filename(), download.bot().name(), f(dccReceiver.size())));
-                                       if (dccReceiver.isRunning()) {
-                                               writer.write(String.format("%.1f%%, %s/s, %s", dccReceiver.progress() * 100.0 / dccReceiver.size(), f(dccReceiver.currentRate()), getTimeLeft(dccReceiver)));
-                                       } else {
-                                               if (dccReceiver.progress() >= dccReceiver.size()) {
-                                                       writer.write(String.format("complete, %s/s", f(dccReceiver.overallRate())));
-                                               } else {
-                                                       writer.write(String.format("aborted at %.1f%%, %s/s", dccReceiver.progress() * 100.0 / dccReceiver.size(), f(dccReceiver.currentRate())));
-                                               }
-                                       }
-                                       writer.write(")\n");
-                               }
-                               writeLine("End of DCCs.");
-                               state = state.setLastDownloads(downloads);
-                       } else if (words[0].equalsIgnoreCase("get")) {
-                               Integer index = Ints.tryParse(words[1]);
-                               if ((index != null) && (index < state.getLastResults().size())) {
-                                       core.fetch(state.getLastResults().get(index).bot(), state.getLastResults().get(index).pack());
-                               }
-                       } else if (words[0].equalsIgnoreCase("cancel")) {
-                               Integer index = Ints.tryParse(words[1]);
-                               if ((index != null) && (index < state.getLastDownloads().size())) {
-                                       core.cancelDownload(state.getLastDownloads().get(index).bot(), state.getLastDownloads().get(index).pack());
-                               }
-                       } else if (words[0].equalsIgnoreCase("stats")) {
-                               int configuredChannelsCount = core.channels().size();
-                               int joinedChannelsCount = core.joinedChannels().size();
-                               int extraChannelsCount = core.extraChannels().size();
-                               Collection<Bot> bots = core.bots();
-                               Set<String> packNames = Sets.newHashSet();
-                               int packsCount = 0;
-                               for (Bot bot : bots) {
-                                       packsCount += bot.packs().size();
-                                       for (Pack pack : bot) {
-                                               packNames.add(pack.name());
-                                       }
-                               }
-
-                               writeLine(String.format("%d channels (%d joined, %d extra), %d bots offering %d packs (%d unique).", configuredChannelsCount, joinedChannelsCount, extraChannelsCount, bots.size(), packsCount, packNames.size()));
-                       } else if (words[0].equalsIgnoreCase("connections")) {
-                               List<Connection> lastConnections = newArrayList();
-                               int counter = 0;
-                               for (Connection connection : core.connections()) {
-                                       lastConnections.add(connection);
-                                       writer.write(String.format("[%d] %s:%d, %s/s\n", counter++, connection.hostname(), connection.port(), f(connection.getInputRate())));
-                               }
-                               writeLine("End of connections.");
-                               state = state.setLastConnections(lastConnections);
-                       } else if (words[0].equalsIgnoreCase("disconnect")) {
-                               if ((words.length == 1) || ("all".equals(words[1]))) {
-                                       for (Connection connection : state.getLastConnections()) {
-                                               core.closeConnection(connection);
-                                       }
-                               } else {
-                                       Integer index = Ints.tryParse(words[1]);
-                                       if ((index != null) && (index < state.getLastConnections().size())) {
-                                               core.closeConnection(state.getLastConnections().get(index));
-                                       }
-                               }
+                       String commandName = words[0];
+                       Collection<Command> eligibleCommands = findEligibleCommands(commandName);
+                       if (eligibleCommands.isEmpty()) {
+                               writeLine(String.format("Invalid command: %s, valid: %s.", commandName, Joiner.on(' ').join(from(commands).transform(TO_NAME))));
+                       } else if (eligibleCommands.size() > 1) {
+                               writeLine(String.format("Commands: %s.", Joiner.on(' ').join(from(eligibleCommands).transform(TO_NAME))));
+                       } else {
+                               Command command = eligibleCommands.iterator().next();
+                               List<String> parameters = from(asList(words)).skip(1).toList();
+                               state = command.execute(state, parameters, writer);
                        }
 
                        lastLine = line;
@@ -290,6 +202,21 @@ public class CommandReader extends AbstractExecutionThreadService {
        // PRIVATE METHODS
        //
 
+       private Collection<Command> findEligibleCommands(String name) {
+               ImmutableSet.Builder<Command> eligibleCommands = ImmutableSet.builder();
+               for (Command command : commands) {
+                       if (command.getName().toLowerCase().startsWith(name.toLowerCase())) {
+                               eligibleCommands.add(command);
+                       }
+                       for (String alias : command.getAliases()) {
+                               if (alias.toLowerCase().startsWith(name.toLowerCase())) {
+                                       eligibleCommands.add(command);
+                               }
+                       }
+               }
+               return eligibleCommands.build();
+       }
+
        /**
         * Writes the given line followed by an LF to the {@link #writer}.
         *
@@ -311,7 +238,7 @@ public class CommandReader extends AbstractExecutionThreadService {
         *              The number to convert
         * @return The converted number
         */
-       private static String f(long number) {
+       static String f(long number) {
                if (number >= (1 << 30)) {
                        return String.format("%.1fG", number / (double) (1 << 30));
                }
@@ -324,271 +251,4 @@ public class CommandReader extends AbstractExecutionThreadService {
                return String.format("%dB", number);
        }
 
-       /**
-        * Returns the estimated time left for the given transfer.
-        *
-        * @param dccReceiver
-        *              The DCC receiver to get the time left for
-        * @return The time left for the transfer, or “unknown” if the time can not be
-        *         estimated
-        */
-       private static String getTimeLeft(DccReceiver dccReceiver) {
-               if ((dccReceiver.size() == -1) || (dccReceiver.currentRate() == 0)) {
-                       return "unknown";
-               }
-               long secondsLeft = (dccReceiver.size() - dccReceiver.progress()) / dccReceiver.currentRate();
-               if (secondsLeft > 3600) {
-                       return String.format("%02d:%02d:%02d", secondsLeft / 3600, (secondsLeft / 60) % 60, secondsLeft % 60);
-               }
-               return String.format("%02d:%02d", (secondsLeft / 60) % 60, secondsLeft % 60);
-       }
-
-       /** Container for result information. */
-       private static class Result implements Comparable<Result> {
-
-               /** {@link Predicate} that matches {@link Result}s that contain an archive. */
-               private static final Predicate<Result> isArchive = new Predicate<Result>() {
-
-                       /** All suffixes that are recognized as archives. */
-                       private final List<String> archiveSuffixes = Arrays.asList("rar", "tar", "zip", "tar.gz", "tar.bz2", "tar.lzma", "7z");
-
-                       @Override
-                       public boolean apply(Result result) {
-                               for (String suffix : archiveSuffixes) {
-                                       if (result.pack().name().toLowerCase().endsWith(suffix)) {
-                                               return true;
-                                       }
-                               }
-                               return false;
-                       }
-               };
-
-               /**
-                * {@link Comparator} for {@link Result}s that sorts archives (as per {@link
-                * #isArchive} to the back of the list.
-                */
-               private static final Comparator<Result> packArchiveComparator = new Comparator<Result>() {
-                       @Override
-                       public int compare(Result leftResult, Result rightResult) {
-                               if (isArchive.apply(leftResult) && !isArchive.apply(rightResult)) {
-                                       return 1;
-                               }
-                               if (!isArchive.apply(leftResult) && isArchive.apply(rightResult)) {
-                                       return -1;
-                               }
-                               return 0;
-                       }
-               };
-
-               /**
-                * {@link Comparator} for bot nicknames. It comprises different strategies:
-                * one name pattern is preferred (and thus listed first), one pattern is
-                * disliked (and thus listed last), the rest is sorted alphabetically.
-                */
-               private static final Comparator<Result> botNameComparator = new Comparator<Result>() {
-
-                       /** Regular expression pattern for preferred names. */
-                       private final Pattern preferredNames = Pattern.compile("(?i)[^\\w]EUR?[^\\w]");
-
-                       /** Regular expression pattern for disliked names. */
-                       private final Pattern dislikedNames = Pattern.compile("(?i)[^\\w]USA?[^\\w]");
-
-                       @Override
-                       public int compare(Result leftResult, Result rightResult) {
-                               String leftBotName = leftResult.bot().name();
-                               String rightBotName = rightResult.bot().name();
-                               /* preferred names to the front! */
-                               if (preferredNames.matcher(leftBotName).find() && !preferredNames.matcher(rightBotName).find()) {
-                                       return -1;
-                               }
-                               if (preferredNames.matcher(rightBotName).find() && !preferredNames.matcher(leftBotName).find()) {
-                                       return 1;
-                               }
-                               /* disliked names to the back. */
-                               if (dislikedNames.matcher(leftBotName).find() && !dislikedNames.matcher(rightBotName).find()) {
-                                       return 1;
-                               }
-                               if (dislikedNames.matcher(rightBotName).find() && !dislikedNames.matcher(leftBotName).find()) {
-                                       return -1;
-                               }
-                               return 0;
-                       }
-               };
-
-               /**
-                * {@link Comparator} for {@link Result}s that sorts them by the name of the
-                * {@link Pack}.
-                */
-               private static final Comparator<Result> packNameComparator = new Comparator<Result>() {
-                       @Override
-                       public int compare(Result leftResult, Result rightResult) {
-                               return leftResult.pack().name().compareToIgnoreCase(rightResult.pack().name());
-                       }
-               };
-
-               /** The bot carrying the pack. */
-               private final Bot bot;
-
-               /** The pack. */
-               private final Pack pack;
-
-               /**
-                * Creates a new result.
-                *
-                * @param bot
-                *              The bot carrying the pack
-                * @param pack
-                *              The pack
-                */
-               private Result(Bot bot, Pack pack) {
-                       this.bot = bot;
-                       this.pack = pack;
-               }
-
-               //
-               // ACCESSORS
-               //
-
-               /**
-                * Returns the bot carrying the pack.
-                *
-                * @return The bot carrying the pack
-                */
-               public Bot bot() {
-                       return bot;
-               }
-
-               /**
-                * Returns the pack.
-                *
-                * @return The pack
-                */
-               public Pack pack() {
-                       return pack;
-               }
-
-               //
-               // COMPARABLE METHODS
-               //
-
-               @Override
-               public int compareTo(Result result) {
-                       return ComparisonChain.start()
-                                       .compare(this, result, packArchiveComparator)
-                                       .compare(this, result, botNameComparator)
-                                       .compare(this, result, packNameComparator).result();
-               }
-
-       }
-
-       /**
-        * Container for the current state of the command reader.
-        *
-        * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
-        */
-       private static class State {
-
-               /** The last connections displayed. */
-               private final List<Connection> lastConnections;
-
-               /** The last results displayed. */
-               private final List<Result> lastResults;
-
-               /** The last downloads displayed. */
-               private final List<Download> lastDownloads;
-
-               /** Creates a new empty state. */
-               public State() {
-                       this(Lists.<Connection>newArrayList(), Lists.<Result>newArrayList(), Lists.<Download>newArrayList());
-               }
-
-               /**
-                * Creates a new state.
-                *
-                * @param lastConnections
-                *              The last connections
-                * @param lastResults
-                *              The last results
-                * @param lastDownloads
-                *              The last downloads
-                */
-               private State(List<Connection> lastConnections, List<Result> lastResults, List<Download> lastDownloads) {
-                       this.lastConnections = lastConnections;
-                       this.lastResults = lastResults;
-                       this.lastDownloads = lastDownloads;
-               }
-
-               //
-               // ACCESSORS
-               //
-
-               /**
-                * Returns the last connections displayed.
-                *
-                * @return The last connections displayed
-                */
-               public List<Connection> getLastConnections() {
-                       return lastConnections;
-               }
-
-               /**
-                * Returns the last results displayed.
-                *
-                * @return The last results displayed
-                */
-               public List<Result> getLastResults() {
-                       return lastResults;
-               }
-
-               /**
-                * Returns the last downloads displayed.
-                *
-                * @return The last downloads displayed
-                */
-               public List<Download> getLastDownloads() {
-                       return lastDownloads;
-               }
-
-               //
-               // MUTATORS
-               //
-
-               /**
-                * Returns a new state with the given last connections and the last downloads
-                * and results of this state.
-                *
-                * @param lastConnections
-                *              The new last connections displayed
-                * @return The new state
-                */
-               public State setLastConnections(List<Connection> lastConnections) {
-                       return new State(lastConnections, lastResults, lastDownloads);
-               }
-
-               /**
-                * Returns a new state with the given last results and the last downloads and
-                * connections of this state.
-                *
-                * @param lastResults
-                *              The new last results displayed
-                * @return The new state
-                */
-               public State setLastResults(List<Result> lastResults) {
-                       return new State(lastConnections, lastResults, lastDownloads);
-               }
-
-               /**
-                * Returns a new state with the given last downloads and the last connections
-                * and results of this state.
-                *
-                * @param lastDownloads
-                *              The new last downloads displayed
-                * @return The new state
-                */
-               public State setLastDownloads(List<Download> lastDownloads) {
-                       return new State(lastConnections, lastResults, lastDownloads);
-               }
-
-       }
-
 }
diff --git a/src/main/java/net/pterodactylus/xdcc/ui/stdin/DisconnectCommand.java b/src/main/java/net/pterodactylus/xdcc/ui/stdin/DisconnectCommand.java
new file mode 100644 (file)
index 0000000..95a64e5
--- /dev/null
@@ -0,0 +1,82 @@
+/*
+ * XdccDownloader - DisconnectCommand.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 <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.xdcc.ui.stdin;
+
+import static java.util.Arrays.asList;
+
+import java.io.IOException;
+import java.io.Writer;
+import java.util.Collection;
+import java.util.List;
+
+import net.pterodactylus.irc.Connection;
+import net.pterodactylus.xdcc.core.Core;
+
+import com.google.common.primitives.Ints;
+
+/**
+ * Command that will disconnect a {@link Connection}.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ * @see State#getLastConnections()
+ */
+public class DisconnectCommand implements Command {
+
+       /** The core to operate on. */
+       private final Core core;
+
+       /**
+        * Creates a new disconnect command.
+        *
+        * @param core
+        *              The core to operate on
+        */
+       public DisconnectCommand(Core core) {
+               this.core = core;
+       }
+
+       //
+       // COMMAND METHODS
+       //
+
+       @Override
+       public String getName() {
+               return "disconnect";
+       }
+
+       @Override
+       public Collection<String> getAliases() {
+               return asList("close");
+       }
+
+       @Override
+       public State execute(State state, List<String> parameters, Writer outputWriter) throws IOException {
+               if ((parameters.isEmpty() || ("all".equals(parameters.get(0))))) {
+                       for (Connection connection : state.getLastConnections()) {
+                               core.closeConnection(connection);
+                       }
+               } else {
+                       Integer index = Ints.tryParse(parameters.get(0));
+                       if ((index != null) && (index < state.getLastConnections().size())) {
+                               core.closeConnection(state.getLastConnections().get(index));
+                       }
+               }
+               return state;
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/xdcc/ui/stdin/DownloadCommand.java b/src/main/java/net/pterodactylus/xdcc/ui/stdin/DownloadCommand.java
new file mode 100644 (file)
index 0000000..2fdec0d
--- /dev/null
@@ -0,0 +1,75 @@
+/*
+ * XdccDownloader - DownloadCommand.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 <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.xdcc.ui.stdin;
+
+import static java.util.Arrays.asList;
+
+import java.io.IOException;
+import java.io.Writer;
+import java.util.Collection;
+import java.util.List;
+
+import net.pterodactylus.xdcc.core.Core;
+
+import com.google.common.primitives.Ints;
+
+/**
+ * Command that requests a download from a bot.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ * @see State#getLastResults()
+ */
+public class DownloadCommand implements Command {
+
+       /** The core to operate on. */
+       private final Core core;
+
+       /**
+        * Creates a new download command.
+        *
+        * @param core
+        *              The core to operate on
+        */
+       public DownloadCommand(Core core) {
+               this.core = core;
+       }
+
+       //
+       // COMMAND METHODS
+       //
+
+       @Override
+       public String getName() {
+               return "download";
+       }
+
+       @Override
+       public Collection<String> getAliases() {
+               return asList("get");
+       }
+
+       @Override
+       public State execute(State state, List<String> parameters, Writer outputWriter) throws IOException {
+               Integer index = Ints.tryParse(parameters.get(0));
+               if ((index != null) && (index < state.getLastResults().size())) {
+                       core.fetch(state.getLastResults().get(index).bot(), state.getLastResults().get(index).pack());
+               }
+               return state;
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/xdcc/ui/stdin/ListConnectionsCommand.java b/src/main/java/net/pterodactylus/xdcc/ui/stdin/ListConnectionsCommand.java
new file mode 100644 (file)
index 0000000..741ed6a
--- /dev/null
@@ -0,0 +1,79 @@
+/*
+ * XdccDownloader - ListConnectionsCommand.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 <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.xdcc.ui.stdin;
+
+import static com.google.common.collect.Lists.newArrayList;
+import static java.util.Collections.emptyList;
+import static net.pterodactylus.xdcc.ui.stdin.CommandReader.f;
+
+import java.io.IOException;
+import java.io.Writer;
+import java.util.Collection;
+import java.util.List;
+
+import net.pterodactylus.irc.Connection;
+import net.pterodactylus.xdcc.core.Core;
+
+/**
+ * Command that will list all current connectios.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class ListConnectionsCommand implements Command {
+
+       /** The core to operate on. */
+       private final Core core;
+
+       /**
+        * Creates a new list connections command.
+        *
+        * @param core
+        *              The core to operate on
+        */
+       public ListConnectionsCommand(Core core) {
+               this.core = core;
+       }
+
+       //
+       // COMMAND METHODS
+       //
+
+       @Override
+       public String getName() {
+               return "connections";
+       }
+
+       @Override
+       public Collection<String> getAliases() {
+               return emptyList();
+       }
+
+       @Override
+       public State execute(State state, List<String> parameters, Writer outputWriter) throws IOException {
+               List<Connection> lastConnections = newArrayList();
+               int counter = 0;
+               for (Connection connection : core.connections()) {
+                       lastConnections.add(connection);
+                       outputWriter.write(String.format("[%d] %s:%d, %s/s\n", counter++, connection.hostname(), connection.port(), f(connection.getInputRate())));
+               }
+               outputWriter.write("End of connections.\n");
+               outputWriter.flush();
+               return state.setLastConnections(lastConnections);
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/xdcc/ui/stdin/ListDownloadsCommand.java b/src/main/java/net/pterodactylus/xdcc/ui/stdin/ListDownloadsCommand.java
new file mode 100644 (file)
index 0000000..753b677
--- /dev/null
@@ -0,0 +1,123 @@
+/*
+ * XdccDownloader - ListDownloadsCommand.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 <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.xdcc.ui.stdin;
+
+import static com.google.common.collect.FluentIterable.from;
+import static com.google.common.collect.Lists.newArrayList;
+import static java.util.Arrays.asList;
+import static net.pterodactylus.xdcc.data.Download.BY_NAME;
+import static net.pterodactylus.xdcc.data.Download.BY_RUNNING;
+import static net.pterodactylus.xdcc.ui.stdin.CommandReader.f;
+
+import java.io.IOException;
+import java.io.Writer;
+import java.util.Collection;
+import java.util.List;
+
+import net.pterodactylus.irc.DccReceiver;
+import net.pterodactylus.xdcc.core.Core;
+import net.pterodactylus.xdcc.data.Download;
+
+import com.google.common.collect.Ordering;
+
+/**
+ * Command that will list all current downloads.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class ListDownloadsCommand implements Command {
+
+       /** The core to operate on. */
+       private final Core core;
+
+       /**
+        * Creates a new list downloads command.
+        *
+        * @param core
+        *              The core to operate on
+        */
+       public ListDownloadsCommand(Core core) {
+               this.core = core;
+       }
+
+       //
+       // COMMAND METHODS
+       //
+
+       @Override
+       public String getName() {
+               return "list";
+       }
+
+       @Override
+       public Collection<String> getAliases() {
+               return asList("dcc");
+       }
+
+       @Override
+       public State execute(State state, List<String> parameters, Writer outputWriter) throws IOException {
+               int counter = 0;
+               List<Download> downloads = newArrayList(from(core.downloads()).toSortedList(Ordering.from(BY_NAME).compound(BY_RUNNING)));
+               for (Download download : downloads) {
+                       DccReceiver dccReceiver = download.dccReceiver();
+                       if (dccReceiver == null) {
+                                               /* download has not even started. */
+                               outputWriter.write(String.format("[%d] %s requested from %s (not started yet)\n", counter++, download.pack().name(), download.bot().name()));
+                               continue;
+                       }
+                       outputWriter.write(String.format("[%d] %s from %s (%s, ", counter++, dccReceiver.filename(), download.bot().name(), f(dccReceiver.size())));
+                       if (dccReceiver.isRunning()) {
+                               outputWriter.write(String.format("%.1f%%, %s/s, %s", dccReceiver.progress() * 100.0 / dccReceiver.size(), f(dccReceiver.currentRate()), getTimeLeft(dccReceiver)));
+                       } else {
+                               if (dccReceiver.progress() >= dccReceiver.size()) {
+                                       outputWriter.write(String.format("complete, %s/s", f(dccReceiver.overallRate())));
+                               } else {
+                                       outputWriter.write(String.format("aborted at %.1f%%, %s/s", dccReceiver.progress() * 100.0 / dccReceiver.size(), f(dccReceiver.currentRate())));
+                               }
+                       }
+                       outputWriter.write(")\n");
+               }
+               outputWriter.write("End of DCCs.\n");
+               outputWriter.flush();
+               return state.setLastDownloads(downloads);
+       }
+
+       //
+       // PRIVATE METHODS
+       //
+
+       /**
+        * Returns the estimated time left for the given transfer.
+        *
+        * @param dccReceiver
+        *              The DCC receiver to get the time left for
+        * @return The time left for the transfer, or “unknown” if the time can not be
+        *         estimated
+        */
+       private static String getTimeLeft(DccReceiver dccReceiver) {
+               if ((dccReceiver.size() == -1) || (dccReceiver.currentRate() == 0)) {
+                       return "unknown";
+               }
+               long secondsLeft = (dccReceiver.size() - dccReceiver.progress()) / dccReceiver.currentRate();
+               if (secondsLeft > 3600) {
+                       return String.format("%02d:%02d:%02d", secondsLeft / 3600, (secondsLeft / 60) % 60, secondsLeft % 60);
+               }
+               return String.format("%02d:%02d", (secondsLeft / 60) % 60, secondsLeft % 60);
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/xdcc/ui/stdin/Result.java b/src/main/java/net/pterodactylus/xdcc/ui/stdin/Result.java
new file mode 100644 (file)
index 0000000..4dea6c5
--- /dev/null
@@ -0,0 +1,171 @@
+/*
+ * XdccDownloader - Result.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 <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.xdcc.ui.stdin;
+
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.List;
+import java.util.regex.Pattern;
+
+import net.pterodactylus.xdcc.data.Bot;
+import net.pterodactylus.xdcc.data.Pack;
+
+import com.google.common.base.Predicate;
+import com.google.common.collect.ComparisonChain;
+
+/**
+ * Container for result information.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class Result implements Comparable<Result> {
+
+       /** {@link Predicate} that matches {@link Result}s that contain an archive. */
+       private static final Predicate<Result> isArchive = new Predicate<Result>() {
+
+               /** All suffixes that are recognized as archives. */
+               private final List<String> archiveSuffixes = Arrays.asList("rar", "tar", "zip", "tar.gz", "tar.bz2", "tar.lzma", "7z");
+
+               @Override
+               public boolean apply(Result result) {
+                       for (String suffix : archiveSuffixes) {
+                               if (result.pack().name().toLowerCase().endsWith(suffix)) {
+                                       return true;
+                               }
+                       }
+                       return false;
+               }
+       };
+
+       /**
+        * {@link Comparator} for {@link Result}s that sorts archives (as per {@link
+        * #isArchive} to the back of the list.
+        */
+       private static final Comparator<Result> packArchiveComparator = new Comparator<Result>() {
+               @Override
+               public int compare(Result leftResult, Result rightResult) {
+                       if (isArchive.apply(leftResult) && !isArchive.apply(rightResult)) {
+                               return 1;
+                       }
+                       if (!isArchive.apply(leftResult) && isArchive.apply(rightResult)) {
+                               return -1;
+                       }
+                       return 0;
+               }
+       };
+
+       /**
+        * {@link Comparator} for bot nicknames. It comprises different strategies: one
+        * name pattern is preferred (and thus listed first), one pattern is disliked
+        * (and thus listed last), the rest is sorted alphabetically.
+        */
+       private static final Comparator<Result> botNameComparator = new Comparator<Result>() {
+
+               /** Regular expression pattern for preferred names. */
+               private final Pattern preferredNames = Pattern.compile("(?i)[^\\w]EUR?[^\\w]");
+
+               /** Regular expression pattern for disliked names. */
+               private final Pattern dislikedNames = Pattern.compile("(?i)[^\\w]USA?[^\\w]");
+
+               @Override
+               public int compare(Result leftResult, Result rightResult) {
+                       String leftBotName = leftResult.bot().name();
+                       String rightBotName = rightResult.bot().name();
+                       /* preferred names to the front! */
+                       if (preferredNames.matcher(leftBotName).find() && !preferredNames.matcher(rightBotName).find()) {
+                               return -1;
+                       }
+                       if (preferredNames.matcher(rightBotName).find() && !preferredNames.matcher(leftBotName).find()) {
+                               return 1;
+                       }
+                       /* disliked names to the back. */
+                       if (dislikedNames.matcher(leftBotName).find() && !dislikedNames.matcher(rightBotName).find()) {
+                               return 1;
+                       }
+                       if (dislikedNames.matcher(rightBotName).find() && !dislikedNames.matcher(leftBotName).find()) {
+                               return -1;
+                       }
+                       return 0;
+               }
+       };
+
+       /**
+        * {@link Comparator} for {@link Result}s that sorts them by the name of the
+        * {@link Pack}.
+        */
+       private static final Comparator<Result> packNameComparator = new Comparator<Result>() {
+               @Override
+               public int compare(Result leftResult, Result rightResult) {
+                       return leftResult.pack().name().compareToIgnoreCase(rightResult.pack().name());
+               }
+       };
+
+       /** The bot carrying the pack. */
+       private final Bot bot;
+
+       /** The pack. */
+       private final Pack pack;
+
+       /**
+        * Creates a new result.
+        *
+        * @param bot
+        *              The bot carrying the pack
+        * @param pack
+        *              The pack
+        */
+       Result(Bot bot, Pack pack) {
+               this.bot = bot;
+               this.pack = pack;
+       }
+
+       //
+       // ACCESSORS
+       //
+
+       /**
+        * Returns the bot carrying the pack.
+        *
+        * @return The bot carrying the pack
+        */
+       public Bot bot() {
+               return bot;
+       }
+
+       /**
+        * Returns the pack.
+        *
+        * @return The pack
+        */
+       public Pack pack() {
+               return pack;
+       }
+
+       //
+       // COMPARABLE METHODS
+       //
+
+       @Override
+       public int compareTo(Result result) {
+               return ComparisonChain.start()
+                               .compare(this, result, packArchiveComparator)
+                               .compare(this, result, botNameComparator)
+                               .compare(this, result, packNameComparator).result();
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/xdcc/ui/stdin/SearchCommand.java b/src/main/java/net/pterodactylus/xdcc/ui/stdin/SearchCommand.java
new file mode 100644 (file)
index 0000000..9619f11
--- /dev/null
@@ -0,0 +1,99 @@
+/*
+ * XdccDownloader - SearchCommand.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 <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.xdcc.ui.stdin;
+
+import static com.google.common.collect.Lists.newArrayList;
+import static java.util.Arrays.asList;
+
+import java.io.IOException;
+import java.io.Writer;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+import net.pterodactylus.xdcc.core.Core;
+import net.pterodactylus.xdcc.data.Bot;
+import net.pterodactylus.xdcc.data.Pack;
+
+/**
+ * Command that searches all {@link Pack}s of all {@link Bot}s for files
+ * matching the search parameters.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class SearchCommand implements Command {
+
+       /** The core to operate on. */
+       private final Core core;
+
+       /**
+        * Creates a new search command.
+        *
+        * @param core
+        *              The core to operate on
+        */
+       public SearchCommand(Core core) {
+               this.core = core;
+       }
+
+       //
+       // COMMAND METHODS
+       //
+
+       @Override
+       public String getName() {
+               return "search";
+       }
+
+       @Override
+       public Collection<String> getAliases() {
+               return asList("find", "locate");
+       }
+
+       @Override
+       public State execute(State state, List<String> parameters, Writer outputWriter) throws IOException {
+               List<Result> lastResult = newArrayList();
+               for (Bot bot : newArrayList(core.bots())) {
+                       for (Pack pack : newArrayList(bot)) {
+                               boolean found = true;
+                               for (String parameter : parameters) {
+                                       if (parameter.startsWith("-") && pack.name().toLowerCase().contains(parameter.toLowerCase().substring(1))) {
+                                               found = false;
+                                               break;
+                                       }
+                                       if (!parameter.startsWith("-") && !pack.name().toLowerCase().contains(parameter.toLowerCase())) {
+                                               found = false;
+                                               break;
+                                       }
+                               }
+                               if (found) {
+                                       lastResult.add(new Result(bot, pack));
+                               }
+                       }
+               }
+               Collections.sort(lastResult);
+               int counter = 0;
+               for (Result result : lastResult) {
+                       outputWriter.write(String.format("[%d] %s (%s) from %s (#%s) on %s\n", counter++, result.pack().name(), result.pack().size(), result.bot().name(), result.pack().id(), result.bot().network().name()));
+               }
+               outputWriter.write("End of Search.\n");
+               outputWriter.flush();
+               return state.setLastResults(lastResult);
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/xdcc/ui/stdin/State.java b/src/main/java/net/pterodactylus/xdcc/ui/stdin/State.java
new file mode 100644 (file)
index 0000000..e292d09
--- /dev/null
@@ -0,0 +1,135 @@
+/*
+ * XdccDownloader - State.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 <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.xdcc.ui.stdin;
+
+import java.util.List;
+
+import net.pterodactylus.irc.Connection;
+import net.pterodactylus.xdcc.data.Download;
+
+import com.google.common.collect.Lists;
+
+/**
+ * Container for the current state of the command reader.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class State {
+
+       /** The last connections displayed. */
+       private final List<Connection> lastConnections;
+
+       /** The last results displayed. */
+       private final List<Result> lastResults;
+
+       /** The last downloads displayed. */
+       private final List<Download> lastDownloads;
+
+       /** Creates a new empty state. */
+       public State() {
+               this(Lists.<Connection>newArrayList(), Lists.<Result>newArrayList(), Lists.<Download>newArrayList());
+       }
+
+       /**
+        * Creates a new state.
+        *
+        * @param lastConnections
+        *              The last connections
+        * @param lastResults
+        *              The last results
+        * @param lastDownloads
+        *              The last downloads
+        */
+       State(List<Connection> lastConnections, List<Result> lastResults, List<Download> lastDownloads) {
+               this.lastConnections = lastConnections;
+               this.lastResults = lastResults;
+               this.lastDownloads = lastDownloads;
+       }
+
+       //
+       // ACCESSORS
+       //
+
+       /**
+        * Returns the last connections displayed.
+        *
+        * @return The last connections displayed
+        */
+       public List<Connection> getLastConnections() {
+               return lastConnections;
+       }
+
+       /**
+        * Returns the last results displayed.
+        *
+        * @return The last results displayed
+        */
+       public List<Result> getLastResults() {
+               return lastResults;
+       }
+
+       /**
+        * Returns the last downloads displayed.
+        *
+        * @return The last downloads displayed
+        */
+       public List<Download> getLastDownloads() {
+               return lastDownloads;
+       }
+
+       //
+       // MUTATORS
+       //
+
+       /**
+        * Returns a new state with the given last connections and the last downloads
+        * and results of this state.
+        *
+        * @param lastConnections
+        *              The new last connections displayed
+        * @return The new state
+        */
+       public State setLastConnections(List<Connection> lastConnections) {
+               return new State(lastConnections, lastResults, lastDownloads);
+       }
+
+       /**
+        * Returns a new state with the given last results and the last downloads and
+        * connections of this state.
+        *
+        * @param lastResults
+        *              The new last results displayed
+        * @return The new state
+        */
+       public State setLastResults(List<Result> lastResults) {
+               return new State(lastConnections, lastResults, lastDownloads);
+       }
+
+       /**
+        * Returns a new state with the given last downloads and the last connections
+        * and results of this state.
+        *
+        * @param lastDownloads
+        *              The new last downloads displayed
+        * @return The new state
+        */
+       public State setLastDownloads(List<Download> lastDownloads) {
+               return new State(lastConnections, lastResults, lastDownloads);
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/xdcc/ui/stdin/StatsCommand.java b/src/main/java/net/pterodactylus/xdcc/ui/stdin/StatsCommand.java
new file mode 100644 (file)
index 0000000..85b92d1
--- /dev/null
@@ -0,0 +1,88 @@
+/*
+ * XdccDownloader - StatsCommand.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 <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.xdcc.ui.stdin;
+
+import static java.util.Collections.emptyList;
+
+import java.io.IOException;
+import java.io.Writer;
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+
+import net.pterodactylus.xdcc.core.Core;
+import net.pterodactylus.xdcc.data.Bot;
+import net.pterodactylus.xdcc.data.Pack;
+
+import com.google.common.collect.Sets;
+
+/**
+ * Command that outputs a short statistic of what is going on.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class StatsCommand implements Command {
+
+       /** The core to operate on. */
+       private final Core core;
+
+       /**
+        * Creates a new stats command.
+        *
+        * @param core
+        *              The core to operate on
+        */
+       public StatsCommand(Core core) {
+               this.core = core;
+       }
+
+       //
+       // COMMAND METHODS
+       //
+
+       @Override
+       public String getName() {
+               return "stats";
+       }
+
+       @Override
+       public Collection<String> getAliases() {
+               return emptyList();
+       }
+
+       @Override
+       public State execute(State state, List<String> parameters, Writer outputWriter) throws IOException {
+               int configuredChannelsCount = core.channels().size();
+               int joinedChannelsCount = core.joinedChannels().size();
+               int extraChannelsCount = core.extraChannels().size();
+               Collection<Bot> bots = core.bots();
+               Set<String> packNames = Sets.newHashSet();
+               int packsCount = 0;
+               for (Bot bot : bots) {
+                       packsCount += bot.packs().size();
+                       for (Pack pack : bot) {
+                               packNames.add(pack.name());
+                       }
+               }
+
+               outputWriter.write(String.format("%d channels (%d joined, %d extra), %d bots offering %d packs (%d unique).\n", configuredChannelsCount, joinedChannelsCount, extraChannelsCount, bots.size(), packsCount, packNames.size()));
+               outputWriter.flush();
+               return state;
+       }
+
+}