Flush output after every command.
[xudocci.git] / src / main / java / net / pterodactylus / xdcc / ui / stdin / CommandReader.java
index 059d63d..2d0a352 100644 (file)
 
 package net.pterodactylus.xdcc.ui.stdin;
 
+import static com.google.common.collect.FluentIterable.from;
+import static java.util.Arrays.asList;
+import static net.pterodactylus.xdcc.ui.stdin.Ansi.bold;
+import static net.pterodactylus.xdcc.ui.stdin.Ansi.green;
+import static net.pterodactylus.xdcc.ui.stdin.Ansi.red;
+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.Collections;
+import java.util.ArrayList;
+import java.util.Collection;
 import java.util.List;
 
-import net.pterodactylus.irc.DccReceiver;
+import net.pterodactylus.irc.util.MessageCleaner;
 import net.pterodactylus.xdcc.core.Core;
-import net.pterodactylus.xdcc.data.Bot;
-import net.pterodactylus.xdcc.data.Pack;
+import net.pterodactylus.xdcc.core.event.DownloadFailed;
+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.Download;
+import net.pterodactylus.xdcc.util.io.DuplicateLineSuppressingWriter;
 
-import com.google.common.collect.Lists;
+import com.google.common.base.Joiner;
+import com.google.common.base.Optional;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.eventbus.Subscribe;
 import com.google.common.util.concurrent.AbstractExecutionThreadService;
 
 /**
@@ -38,14 +56,15 @@ 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;
 
        /** The writer to write the results to. */
-       private final Writer writer;
+       private final DuplicateLineSuppressingWriter writer;
+       private final Collection<Download> failedDownloads;
 
        /**
         * Creates a new command reader.
@@ -57,10 +76,24 @@ public class CommandReader extends AbstractExecutionThreadService {
         * @param writer
         *              The write to write results to
         */
-       public CommandReader(Core core, Reader reader, Writer writer) {
-               this.core = core;
+       public CommandReader(Core core, Reader reader, Writer writer, Collection<Download> failedDownloads) {
                this.reader = new BufferedReader(reader);
-               this.writer = writer;
+               this.writer = new DuplicateLineSuppressingWriter(writer);
+               this.failedDownloads = failedDownloads;
+
+               /* 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));
+               commandBuilder.add(new FailedDownloadsCommand(failedDownloads));
+               commandBuilder.add(new RestartCommand(core, failedDownloads));
+               commandBuilder.add(new ResearchCommand(core));
+               commands = commandBuilder.build();
        }
 
        //
@@ -71,104 +104,193 @@ public class CommandReader extends AbstractExecutionThreadService {
        protected void run() throws Exception {
                String lastLine = "";
                String line;
-               final List<Result> lastResult = Lists.newArrayList();
+               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;
                        }
+                       writer.reset();
                        String[] words = line.split(" +");
-                       if (words[0].equalsIgnoreCase("search")) {
-                               lastResult.clear();
-                               for (Bot bot : core.bots()) {
-                                       for (Pack pack : 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) {
-                                       writer.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()));
-                               }
-                               writer.write("End of Search.\n");
-                       } else if (words[0].equalsIgnoreCase("dcc")) {
-                               int counter = 0;
-                               for (DccReceiver dccReceiver : core.dccReceivers()) {
-                                       writer.write(String.format("[%d] %s (%s, %d%%%s)\n", counter++, dccReceiver.filename(), dccReceiver.size(), dccReceiver.progress() * 100 / dccReceiver.size(), dccReceiver.isRunning() ? "" : ", finished"));
-                               }
-                               writer.write("End of DCCs.\n");
+                       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);
+                               writer.flush();
                        }
 
                        lastLine = line;
-                       writer.flush();
                }
        }
 
-       /** Container for result information. */
-       private static class Result implements Comparable<Result> {
-
-               /** 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;
+       //
+       // EVENT HANDLERS
+       //
+
+       /**
+        * Called when a download was started.
+        *
+        * @param downloadStarted
+        *              The download started event
+        */
+       @Subscribe
+       public void downloadStarted(DownloadStarted downloadStarted) {
+               Download download = downloadStarted.download();
+               try {
+                       writeLine(String.format("Download of %s (from %s, %s) has started.", bold(download.pack().name()), download.bot().name(), download.bot().network().name()));
+               } catch (IOException ioe1) {
+                       /* ignore. */
+               }
+       }
+
+       /**
+        * Called when a download is finished.
+        *
+        * @param downloadFinished
+        *              The download finished event
+        */
+       @Subscribe
+       public void downloadFinished(DownloadFinished downloadFinished) {
+               Download download = downloadFinished.download();
+               removeFailedDownloads(download.pack().name());
+               try {
+                       writeLine(green(String.format("Download of %s (from %s, %s) has finished, at %s/s.", download.pack().name(), download.bot().name(), download.bot().network().name(), f(download.dccReceiver().overallRate()))));
+               } catch (IOException ioe1) {
+                       /* ignore. */
+               }
+       }
+
+       private void removeFailedDownloads(String name) {
+               List<Download> failedDownloadsToRemove = new ArrayList<>();
+               for (Download failedDownload : failedDownloads) {
+                       if (failedDownload.pack().name().equals(name)) {
+                               failedDownloadsToRemove.add(failedDownload);
+                       }
+               }
+               failedDownloads.removeAll(failedDownloadsToRemove);
+       }
+
+       /**
+        * Called when a download fails.
+        *
+        * @param downloadFailed
+        *              The download failed event
+        */
+       @Subscribe
+       public void downloadFailed(DownloadFailed downloadFailed) {
+               Download download = downloadFailed.download();
+               failedDownloads.add(download);
+               try {
+                       writeLine(red(String.format("Download of %s (from %s, %s) has failed at %.1f%% and %s/s.", download.filename(), download.bot().name(), download.bot().network().name(), download.dccReceiver().progress() * 100.0 / download.dccReceiver().size(), f(download.dccReceiver().overallRate()))));
+               } catch (IOException ioe1) {
+                       /* ignore. */
+               }
+       }
+
+       /**
+        * Displays the received message on the console.
+        *
+        * @param messageReceived
+        *              The message received event
+        */
+       @Subscribe
+       public void messageReceived(MessageReceived messageReceived) {
+               try {
+                       writeLine(String.format("Message from %s: %s", messageReceived.source(), MessageCleaner.getDefaultInstance().clean(messageReceived.message())));
+               } catch (IOException e) {
+                       /* ignore. */
                }
+       }
 
-               //
-               // ACCESSORS
-               //
-
-               /**
-                * Returns the bot carrying the pack.
-                *
-                * @return The bot carrying the pack
-                */
-               public Bot bot() {
-                       return bot;
+       /**
+        * Writes a generic message to the console.
+        *
+        * @param genericMessage
+        *              The generic message event
+        */
+       @Subscribe
+       public void genericMessage(GenericMessage genericMessage) {
+               try {
+                       writeLine(genericMessage.message());
+               } catch (IOException ioe1) {
+                       /* ignore. */
                }
+       }
 
-               /**
-                * Returns the pack.
-                *
-                * @return The pack
-                */
-               public Pack pack() {
-                       return pack;
+       //
+       // 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();
+       }
 
-               //
-               // COMPARABLE METHODS
-               //
+       /**
+        * Writes the given line followed by an LF to the {@link #writer}.
+        *
+        * @param line
+        *              The line to write
+        * @throws IOException
+        *              if an I/O error occurs
+        */
+       private void writeLine(String line) throws IOException {
+               writer.write(line + "\n");
+               writer.flush();
+       }
 
-               @Override
-               public int compareTo(Result result) {
-                       return pack().name().compareToIgnoreCase(result.pack().name());
+       /**
+        * Converts large numbers into a human-friendly format, by showing SI prefixes
+        * for ×1024 (K), ×1048576 (M), and ×1073741824 (G).
+        *
+        * @param number
+        *              The number to convert
+        * @return The converted number
+        */
+       static String f(long number) {
+               if (number >= (1 << 30)) {
+                       return String.format("%.1fG", number / (double) (1 << 30));
+               }
+               if (number >= (1 << 20)) {
+                       return String.format("%.1fM", number / (double) (1 << 20));
                }
+               if (number >= (1 << 10)) {
+                       return String.format("%.1fK", number / (double) (1 << 10));
+               }
+               return String.format("%dB", number);
+       }
 
+       /**
+        * Formats the given number of seconds into a more easily readable string.
+        *
+        * @param seconds
+        *              The number of seconds
+        * @return The formatted time, or “unknown” if the time is unknown
+        */
+       static String t(Optional<Long> seconds) {
+               if (!seconds.isPresent()) {
+                       return "unknown";
+               }
+               if (seconds.get() > 3600) {
+                       return String.format("%02d:%02d:%02d", seconds.get() / 3600, (seconds.get() / 60) % 60, seconds.get() % 60);
+               }
+               return String.format("%02d:%02d", (seconds.get() / 60) % 60, seconds.get() % 60);
        }
 
 }