♻️ Move output generation to state
authorDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Mon, 21 Sep 2020 23:12:14 +0000 (01:12 +0200)
committerDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Mon, 21 Sep 2020 23:12:14 +0000 (01:12 +0200)
29 files changed:
build.gradle
src/main/java/net/pterodactylus/rhynodge/State.java
src/main/java/net/pterodactylus/rhynodge/Trigger.java
src/main/java/net/pterodactylus/rhynodge/engine/ReactionRunner.java
src/main/java/net/pterodactylus/rhynodge/filters/ComicSiteFilter.java
src/main/java/net/pterodactylus/rhynodge/filters/webpages/savoy/SavoyTicketsFilter.java
src/main/java/net/pterodactylus/rhynodge/states/AbstractState.java
src/main/java/net/pterodactylus/rhynodge/states/ComicState.java
src/main/java/net/pterodactylus/rhynodge/states/EpisodeState.java
src/main/java/net/pterodactylus/rhynodge/states/FailedState.java
src/main/java/net/pterodactylus/rhynodge/states/FileState.java
src/main/java/net/pterodactylus/rhynodge/states/HtmlState.java
src/main/java/net/pterodactylus/rhynodge/states/HttpState.java
src/main/java/net/pterodactylus/rhynodge/states/OutputState.java
src/main/java/net/pterodactylus/rhynodge/states/StringState.java
src/main/java/net/pterodactylus/rhynodge/states/TorrentState.java
src/main/java/net/pterodactylus/rhynodge/triggers/AlwaysTrigger.java
src/main/java/net/pterodactylus/rhynodge/triggers/FileExistenceTrigger.java
src/main/java/net/pterodactylus/rhynodge/triggers/FileStateModifiedTrigger.java
src/main/java/net/pterodactylus/rhynodge/triggers/NewComicTrigger.java
src/main/java/net/pterodactylus/rhynodge/triggers/NewEpisodeTrigger.java
src/main/java/net/pterodactylus/rhynodge/triggers/NewTorrentTrigger.java
src/main/kotlin/net/pterodactylus/rhynodge/webpages/weather/WeatherState.kt
src/main/kotlin/net/pterodactylus/rhynodge/webpages/weather/WeatherTrigger.kt
src/test/java/net/pterodactylus/rhynodge/states/StateManagerTest.java
src/test/kotlin/net/pterodactylus/rhynodge/queries/FallbackQueryTest.kt
src/test/kotlin/net/pterodactylus/rhynodge/triggers/AlwaysTriggerTest.kt
src/test/kotlin/net/pterodactylus/rhynodge/webpages/weather/WeatherTriggerTest.kt
src/test/kotlin/net/pterodactylus/rhynodge/webpages/weather/wetterde/WetterDeFilterTest.kt

index 814add9..582c0ff 100644 (file)
@@ -46,6 +46,7 @@ dependencies {
     compile group: "com.fasterxml.jackson.core", name: "jackson-databind", version: "2.1.2"
     compile group: "com.google.inject", name: "guice", version: "4.0"
     compile group: "org.jetbrains.kotlinx", name: "kotlinx-html-jvm", version: "0.7.1"
+    compile group: 'com.google.code.findbugs', name: 'jsr305', version: '3.0.2'
 
     testCompile group: "junit", name: "junit", version:"4.12"
     testCompile group: "org.hamcrest", name: "hamcrest-library", version:"1.3"
index d549d69..bda5bf5 100644 (file)
 
 package net.pterodactylus.rhynodge;
 
+import javax.annotation.Nonnull;
+
+import net.pterodactylus.rhynodge.output.Output;
+
 /**
  * Defines the current state of a system.
  *
@@ -76,4 +80,7 @@ public interface State {
         */
        Throwable exception();
 
+       @Nonnull
+       Output output(Reaction reaction);
+
 }
index e4024f2..9f43a46 100644 (file)
@@ -17,7 +17,6 @@
 
 package net.pterodactylus.rhynodge;
 
-import net.pterodactylus.rhynodge.output.Output;
 import net.pterodactylus.rhynodge.states.FileState;
 
 /**
@@ -53,14 +52,4 @@ public interface Trigger {
         */
        boolean triggers();
 
-       /**
-        * Returns the output of this trigger. This will only return a meaningful
-        * value if {@link #triggers()} returns {@code true}.
-        *
-        * @param reaction
-        *            The reaction being triggered
-        * @return The output of this trigger
-        */
-       Output output(Reaction reaction);
-
 }
index f4ae72e..a4006d8 100644 (file)
@@ -64,7 +64,7 @@ public class ReactionRunner implements Runnable {
                reactionState.saveState(newState);
                if (trigger.triggers()) {
                        logger.info(format("Trigger was hit for %s, executing action...", reaction.name()));
-                       reaction.action().execute(trigger.output(reaction));
+                       reaction.action().execute(newState.output(reaction));
                }
                logger.info(format("Reaction %s finished.", reaction.name()));
        }
index 818f711..6bc67cd 100644 (file)
@@ -21,6 +21,7 @@ import static com.google.common.base.Preconditions.checkArgument;
 
 import java.net.URI;
 import java.net.URISyntaxException;
+import java.util.Collections;
 import java.util.List;
 
 import net.pterodactylus.rhynodge.Filter;
@@ -61,7 +62,6 @@ public abstract class ComicSiteFilter implements Filter {
                        return new FailedState();
                }
 
-               ComicState comicState = new ComicState();
                Comic comic = new Comic(title.or(""));
                int imageCounter = 0;
                for (String imageUrl : imageUrls) {
@@ -75,9 +75,8 @@ public abstract class ComicSiteFilter implements Filter {
                                throw new IllegalStateException(String.format("Could not resolve image URL “%s” against base URL “%s”.", imageUrl, htmlState.uri()), use1);
                        }
                }
-               comicState.add(comic);
 
-               return comicState;
+               return new ComicState(Collections.singletonList(comic));
        }
 
        //
index 14c6c22..c418a4c 100644 (file)
@@ -19,9 +19,6 @@ import java.util.stream.Collectors;
 
 import net.pterodactylus.rhynodge.Filter;
 import net.pterodactylus.rhynodge.State;
-import net.pterodactylus.rhynodge.filters.webpages.savoy.Movie;
-import net.pterodactylus.rhynodge.filters.webpages.savoy.MovieExtractor;
-import net.pterodactylus.rhynodge.filters.webpages.savoy.TicketLink;
 import net.pterodactylus.rhynodge.states.HtmlState;
 import net.pterodactylus.rhynodge.states.OutputState;
 
index afb7dc2..081fbcc 100644 (file)
 
 package net.pterodactylus.rhynodge.states;
 
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import net.pterodactylus.rhynodge.Reaction;
 import net.pterodactylus.rhynodge.State;
+import net.pterodactylus.rhynodge.output.DefaultOutput;
+import net.pterodactylus.rhynodge.output.Output;
 
 import com.fasterxml.jackson.annotation.JsonProperty;
 import com.fasterxml.jackson.annotation.JsonTypeInfo;
+import com.google.common.escape.Escaper;
+import com.google.common.html.HtmlEscapers;
 
 /**
  * Abstract implementation of a {@link State} that knows about the basic
@@ -143,4 +151,29 @@ public abstract class AbstractState implements State {
                return exception;
        }
 
+       @Nonnull
+       @Override
+       public Output output(Reaction reaction) {
+               return new DefaultOutput(summary(reaction))
+                               .addText("text/plain", plainText())
+                               .addText("text/html", htmlText());
+       }
+
+       @Nonnull
+       protected String summary(Reaction reaction) {
+               return reaction.name();
+       }
+
+       @Nonnull
+       protected abstract String plainText();
+
+       @Nullable
+       protected String htmlText() {
+               //noinspection UnstableApiUsage
+               return "<div>" + htmlEscaper.escape(plainText()) + "</div>";
+       }
+
+       @SuppressWarnings("UnstableApiUsage")
+       private static final Escaper htmlEscaper = HtmlEscapers.htmlEscaper();
+
 }
index 7e80686..e1eff9a 100644 (file)
 
 package net.pterodactylus.rhynodge.states;
 
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
+import java.util.Set;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
 
+import net.pterodactylus.rhynodge.Reaction;
 import net.pterodactylus.rhynodge.states.ComicState.Comic;
 
 import com.fasterxml.jackson.annotation.JsonProperty;
 import com.google.common.collect.Lists;
+import org.apache.commons.lang3.StringEscapeUtils;
+import org.apache.commons.lang3.StringUtils;
+
+import static java.lang.String.format;
 
 /**
  * {@link net.pterodactylus.rhynodge.State} that can store an arbitrary amout of
@@ -35,6 +47,21 @@ public class ComicState extends AbstractState implements Iterable<Comic> {
 
        @JsonProperty
        private final List<Comic> comics = Lists.newArrayList();
+       private final Set<Comic> newComics = new HashSet<>();
+
+       @SuppressWarnings("unused")
+       // used for deserialization
+       private ComicState() {
+       }
+
+       public ComicState(Collection<Comic> allComics) {
+               this.comics.addAll(allComics);
+       }
+
+       public ComicState(Collection<Comic> allComics, Collection<Comic> newComics) {
+               this(allComics);
+               this.newComics.addAll(newComics);
+       }
 
        @Override
        public boolean isEmpty() {
@@ -45,11 +72,6 @@ public class ComicState extends AbstractState implements Iterable<Comic> {
                return comics;
        }
 
-       public ComicState add(Comic comic) {
-               comics.add(comic);
-               return this;
-       }
-
        @Override
        public Iterator<Comic> iterator() {
                return comics.iterator();
@@ -57,7 +79,69 @@ public class ComicState extends AbstractState implements Iterable<Comic> {
 
        @Override
        public String toString() {
-               return String.format("ComicState[comics=%s]", comics());
+               return format("ComicState[comics=%s]", comics());
+       }
+
+       @Nonnull
+       @Override
+       protected String summary(Reaction reaction) {
+               return format("New Comic found for “%s!”", reaction.name());
+       }
+
+       @Nonnull
+       @Override
+       protected String plainText() {
+               StringBuilder text = new StringBuilder();
+
+               for (Comic newComic : newComics) {
+                       text.append("Comic Found: ").append(newComic.title()).append("\n\n");
+                       for (Strip strip : newComic) {
+                               text.append("Image: ").append(strip.imageUrl()).append("\n");
+                               if (!StringUtils.isBlank(strip.comment())) {
+                                       text.append("Comment: ").append(strip.comment()).append("\n");
+                               }
+                       }
+                       text.append("\n\n");
+               }
+
+               return text.toString();
+       }
+
+       @Nullable
+       @Override
+       protected String htmlText() {
+               StringBuilder html = new StringBuilder();
+               html.append("<body>");
+
+               for (Comic newComic : newComics) {
+                       generateComicHtml(html, newComic);
+               }
+
+               List<Comic> latestComics = new ArrayList<>(comics());
+               Collections.reverse(latestComics);
+               int comicCount = 0;
+               for (Comic comic : latestComics) {
+                       if (newComics.contains(comic)) {
+                               continue;
+                       }
+                       generateComicHtml(html, comic);
+                       if (++comicCount == 7) {
+                               break;
+                       }
+               }
+
+               return html.append("</body>").toString();
+       }
+
+       private void generateComicHtml(StringBuilder html, Comic comic) {
+               html.append("<h1>").append(StringEscapeUtils.escapeHtml4(comic.title())).append("</h1>\n");
+               for (Strip strip : comic) {
+                       html.append("<div><img src=\"").append(StringEscapeUtils.escapeHtml4(strip.imageUrl()));
+                       html.append("\" alt=\"").append(StringEscapeUtils.escapeHtml4(strip.comment()));
+                       html.append("\" title=\"").append(StringEscapeUtils.escapeHtml4(strip.comment()));
+                       html.append("\"></div>\n");
+                       html.append("<div>").append(StringEscapeUtils.escapeHtml4(strip.comment())).append("</div>\n");
+               }
        }
 
        /**
@@ -111,7 +195,7 @@ public class ComicState extends AbstractState implements Iterable<Comic> {
 
                @Override
                public String toString() {
-                       return String.format("Comic[title=%s,strips=%s]", title(), strips());
+                       return format("Comic[title=%s,strips=%s]", title(), strips());
                }
 
        }
@@ -158,7 +242,7 @@ public class ComicState extends AbstractState implements Iterable<Comic> {
 
                @Override
                public String toString() {
-                       return String.format("Strip[imageUrl=%s,comment=%s]", imageUrl(), comment());
+                       return format("Strip[imageUrl=%s,comment=%s]", imageUrl(), comment());
                }
 
        }
index 78ca478..e022541 100644 (file)
@@ -20,9 +20,15 @@ package net.pterodactylus.rhynodge.states;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
 
+import net.pterodactylus.rhynodge.Reaction;
 import net.pterodactylus.rhynodge.State;
 import net.pterodactylus.rhynodge.filters.EpisodeFilter;
 import net.pterodactylus.rhynodge.states.EpisodeState.Episode;
@@ -30,6 +36,10 @@ import net.pterodactylus.rhynodge.states.TorrentState.TorrentFile;
 
 import com.fasterxml.jackson.annotation.JsonProperty;
 import com.google.common.base.Function;
+import com.google.common.collect.FluentIterable;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Ordering;
+import org.apache.commons.lang3.StringEscapeUtils;
 
 /**
  * {@link State} implementation that stores episodes of TV shows, parsed via
@@ -39,28 +49,38 @@ import com.google.common.base.Function;
  */
 public class EpisodeState extends AbstractState implements Iterable<Episode> {
 
-       /** The episodes found in the current request. */
+       /**
+        * The episodes found in the current request.
+        */
        @JsonProperty
-       private final List<Episode> episodes = new ArrayList<Episode>();
+       private final List<Episode> episodes = new ArrayList<>();
+       private final Set<Episode> newEpisodes = new HashSet<>();
+       private final Set<Episode> changedEpisodes = new HashSet<>();
+       private final Set<TorrentFile> newTorrentFiles = new HashSet<>();
 
        /**
         * No-arg constructor for deserialization.
         */
        @SuppressWarnings("unused")
        private EpisodeState() {
-               this(Collections.<Episode> emptySet());
        }
 
        /**
         * Creates a new episode state.
         *
-        * @param episodes
-        *            The episodes of the request
+        * @param episodes The episodes of the request
         */
        public EpisodeState(Collection<Episode> episodes) {
                this.episodes.addAll(episodes);
        }
 
+       public EpisodeState(Collection<Episode> episodes, Collection<Episode> newEpisodes, Collection<Episode> changedEpisodes, Collection<TorrentFile> newTorreFiles) {
+               this(episodes);
+               this.newEpisodes.addAll(newEpisodes);
+               this.changedEpisodes.addAll(changedEpisodes);
+               this.newTorrentFiles.addAll(newTorreFiles);
+       }
+
        //
        // ACCESSORS
        //
@@ -79,6 +99,124 @@ public class EpisodeState extends AbstractState implements Iterable<Episode> {
                return Collections.unmodifiableCollection(episodes);
        }
 
+       @Nonnull
+       @Override
+       protected String summary(Reaction reaction) {
+               if (!newEpisodes.isEmpty()) {
+                       if (!changedEpisodes.isEmpty()) {
+                               return String.format("%d new and %d changed Torrent(s) for “%s!”", newEpisodes.size(), changedEpisodes.size(), reaction.name());
+                       }
+                       return String.format("%d new Torrent(s) for “%s!”", newEpisodes.size(), reaction.name());
+               }
+               return String.format("%d changed Torrent(s) for “%s!”", changedEpisodes.size(), reaction.name());
+       }
+
+       @Nonnull
+       @Override
+       protected String plainText() {
+               StringBuilder stringBuilder = new StringBuilder();
+               if (!newEpisodes.isEmpty()) {
+                       stringBuilder.append("New Episodes\n\n");
+                       for (Episode episode : newEpisodes) {
+                               stringBuilder.append("- ").append(episode.identifier()).append("\n");
+                               for (TorrentFile torrentFile : episode) {
+                                       stringBuilder.append("  - ").append(torrentFile.name()).append(", ").append(torrentFile.size()).append("\n");
+                                       if ((torrentFile.magnetUri() != null) && (torrentFile.magnetUri().length() > 0)) {
+                                               stringBuilder.append("    Magnet: ").append(torrentFile.magnetUri()).append("\n");
+                                       }
+                                       if ((torrentFile.downloadUri() != null) && (torrentFile.downloadUri().length() > 0)) {
+                                               stringBuilder.append("    Download: ").append(torrentFile.downloadUri()).append("\n");
+                                       }
+                               }
+                       }
+               }
+               if (!changedEpisodes.isEmpty()) {
+                       stringBuilder.append("Changed Episodes\n\n");
+                       for (Episode episode : changedEpisodes) {
+                               stringBuilder.append("- ").append(episode.identifier()).append("\n");
+                               for (TorrentFile torrentFile : episode) {
+                                       stringBuilder.append("  - ").append(torrentFile.name()).append(", ").append(torrentFile.size()).append("\n");
+                                       if ((torrentFile.magnetUri() != null) && (torrentFile.magnetUri().length() > 0)) {
+                                               stringBuilder.append("    Magnet: ").append(torrentFile.magnetUri()).append("\n");
+                                       }
+                                       if ((torrentFile.downloadUri() != null) && (torrentFile.downloadUri().length() > 0)) {
+                                               stringBuilder.append("    Download: ").append(torrentFile.downloadUri()).append("\n");
+                                       }
+                               }
+                       }
+               }
+               /* list all known episodes. */
+               stringBuilder.append("All Known Episodes\n\n");
+               ImmutableMap<Integer, Collection<Episode>> episodesBySeason = FluentIterable.from(episodes).index(Episode::season).asMap();
+               for (Map.Entry<Integer, Collection<Episode>> seasonEntry : episodesBySeason.entrySet()) {
+                       stringBuilder.append("  Season ").append(seasonEntry.getKey()).append("\n\n");
+                       for (Episode episode : Ordering.natural().sortedCopy(seasonEntry.getValue())) {
+                               stringBuilder.append("    Episode ").append(episode.episode()).append("\n");
+                               for (TorrentFile torrentFile : episode) {
+                                       stringBuilder.append("      Size: ").append(torrentFile.size());
+                                       stringBuilder.append(" in ").append(torrentFile.fileCount()).append(" file(s): ");
+                                       stringBuilder.append(torrentFile.magnetUri());
+                               }
+                       }
+               }
+               return stringBuilder.toString();
+       }
+
+       @Nullable
+       @Override
+       protected String htmlText() {
+               StringBuilder htmlBuilder = new StringBuilder();
+               htmlBuilder.append("<html><body>\n");
+               /* show all known episodes. */
+               htmlBuilder.append("<table>\n<caption>All Known Episodes</caption>\n");
+               htmlBuilder.append("<thead>\n");
+               htmlBuilder.append("<tr>");
+               htmlBuilder.append("<th>Season</th>");
+               htmlBuilder.append("<th>Episode</th>");
+               htmlBuilder.append("<th>Filename</th>");
+               htmlBuilder.append("<th>Size</th>");
+               htmlBuilder.append("<th>File(s)</th>");
+               htmlBuilder.append("<th>Seeds</th>");
+               htmlBuilder.append("<th>Leechers</th>");
+               htmlBuilder.append("<th>Magnet</th>");
+               htmlBuilder.append("<th>Download</th>");
+               htmlBuilder.append("</tr>\n");
+               htmlBuilder.append("</thead>\n");
+               htmlBuilder.append("<tbody>\n");
+               Episode lastEpisode = null;
+               for (Map.Entry<Integer, Collection<Episode>> seasonEntry : FluentIterable.from(Ordering.natural().reverse().sortedCopy(episodes)).index(Episode.BY_SEASON).asMap().entrySet()) {
+                       for (Episode episode : seasonEntry.getValue()) {
+                               for (TorrentFile torrentFile : episode) {
+                                       if (newEpisodes.contains(episode)) {
+                                               htmlBuilder.append("<tr style=\"color: #008000; font-weight: bold;\">");
+                                       } else if (newTorrentFiles.contains(torrentFile)) {
+                                               htmlBuilder.append("<tr style=\"color: #008000;\">");
+                                       } else {
+                                               htmlBuilder.append("<tr>");
+                                       }
+                                       if ((lastEpisode == null) || !lastEpisode.equals(episode)) {
+                                               htmlBuilder.append("<td>").append(episode.season()).append("</td><td>").append(episode.episode()).append("</td>");
+                                       } else {
+                                               htmlBuilder.append("<td colspan=\"2\"></td>");
+                                       }
+                                       htmlBuilder.append("<td>").append(StringEscapeUtils.escapeHtml4(torrentFile.name())).append("</td>");
+                                       htmlBuilder.append("<td>").append(StringEscapeUtils.escapeHtml4(torrentFile.size())).append("</td>");
+                                       htmlBuilder.append("<td>").append(torrentFile.fileCount()).append("</td>");
+                                       htmlBuilder.append("<td>").append(torrentFile.seedCount()).append("</td>");
+                                       htmlBuilder.append("<td>").append(torrentFile.leechCount()).append("</td>");
+                                       htmlBuilder.append("<td><a href=\"").append(StringEscapeUtils.escapeHtml4(torrentFile.magnetUri())).append("\">Link</a></td>");
+                                       htmlBuilder.append("<td><a href=\"").append(StringEscapeUtils.escapeHtml4(torrentFile.downloadUri())).append("\">Link</a></td>");
+                                       htmlBuilder.append("</tr>\n");
+                                       lastEpisode = episode;
+                               }
+                       }
+               }
+               htmlBuilder.append("</tbody>\n");
+               htmlBuilder.append("</table>\n");
+               htmlBuilder.append("</body></html>\n");
+               return htmlBuilder.toString();
+       }
+
        //
        // ITERABLE INTERFACE
        //
index a9dbba5..6c1d844 100644 (file)
 
 package net.pterodactylus.rhynodge.states;
 
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.io.Writer;
+import javax.annotation.Nonnull;
+
 import net.pterodactylus.rhynodge.State;
 
 /**
@@ -51,6 +57,21 @@ public class FailedState extends AbstractState {
                return true;
        }
 
+       @Nonnull
+       @Override
+       protected String plainText() {
+               if (exception() == null) {
+                       return "Failed";
+               }
+               try (Writer stringWriter = new StringWriter();
+                        PrintWriter printWriter = new PrintWriter(stringWriter)) {
+                       exception().printStackTrace(printWriter);
+                       return "Failed: " + stringWriter.toString();
+               } catch (IOException ioe1) {
+                       return "Failed while rendering exception";
+               }
+       }
+
        //
        // STATIC METHODS
        //
index 0c7b4cb..983500d 100644 (file)
@@ -17,6 +17,8 @@
 
 package net.pterodactylus.rhynodge.states;
 
+import javax.annotation.Nonnull;
+
 import net.pterodactylus.rhynodge.State;
 
 /**
@@ -119,6 +121,12 @@ public class FileState extends AbstractState {
                return modificationTime;
        }
 
+       @Nonnull
+       @Override
+       protected String plainText() {
+               return toString();
+       }
+
        //
        // OBJECT METHODS
        //
index d2731f7..83cd36d 100644 (file)
@@ -17,6 +17,9 @@
 
 package net.pterodactylus.rhynodge.states;
 
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
 import net.pterodactylus.rhynodge.State;
 
 import org.jsoup.nodes.Document;
@@ -74,6 +77,19 @@ public class HtmlState extends AbstractState {
                return document;
        }
 
+       @Nonnull
+       @Override
+       protected String plainText() {
+               //noinspection ConstantConditions
+               return htmlText();
+       }
+
+       @Nullable
+       @Override
+       protected String htmlText() {
+               return document.toString();
+       }
+
        //
        // OBJECT METHODS
        //
index ea733f7..76dbc94 100644 (file)
@@ -21,6 +21,8 @@ import static java.util.Arrays.copyOf;
 
 import java.io.UnsupportedEncodingException;
 
+import javax.annotation.Nonnull;
+
 import net.pterodactylus.rhynodge.State;
 import net.pterodactylus.rhynodge.queries.HttpQuery;
 
@@ -126,6 +128,12 @@ public class HttpState extends AbstractState {
                }
        }
 
+       @Nonnull
+       @Override
+       protected String plainText() {
+               return content();
+       }
+
        //
        // STATIC METHODS
        //
index 89d1fcb..f5d93f3 100644 (file)
@@ -2,6 +2,9 @@ package net.pterodactylus.rhynodge.states;
 
 import java.util.Optional;
 
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
 import net.pterodactylus.rhynodge.State;
 
 import com.fasterxml.jackson.annotation.JsonProperty;
@@ -31,12 +34,16 @@ public class OutputState extends AbstractState {
                return !plainTextOutput.isPresent() && !htmlOutput.isPresent();
        }
 
-       public Optional<String> plainTextOutput() {
-               return plainTextOutput;
+       @Nonnull
+       @Override
+       protected String plainText() {
+               return plainTextOutput.orElse("");
        }
 
-       public Optional<String> htmlOutput() {
-               return htmlOutput;
+       @Nullable
+       @Override
+       protected String htmlText() {
+               return htmlOutput.orElse(null);
        }
 
 }
index 12c21a7..ad989b3 100644 (file)
@@ -17,6 +17,8 @@
 
 package net.pterodactylus.rhynodge.states;
 
+import javax.annotation.Nonnull;
+
 /**
  * A {@link net.pterodactylus.rhynodge.State} that stores a single {@link
  * String} value.
@@ -52,4 +54,10 @@ public class StringState extends AbstractState {
                return value.isEmpty();
        }
 
+       @Nonnull
+       @Override
+       protected String plainText() {
+               return value;
+       }
+
 }
index 3a7bf54..f2e9ae2 100644 (file)
@@ -20,18 +20,31 @@ package net.pterodactylus.rhynodge.states;
 import java.nio.charset.StandardCharsets;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Optional;
+import java.util.Set;
 
+import javax.annotation.Nonnull;
+
+import net.pterodactylus.rhynodge.Reaction;
 import net.pterodactylus.rhynodge.State;
+import net.pterodactylus.rhynodge.output.DefaultOutput;
+import net.pterodactylus.rhynodge.output.Output;
 import net.pterodactylus.rhynodge.states.TorrentState.TorrentFile;
 
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Ordering;
+import org.apache.commons.lang3.StringEscapeUtils;
 import org.apache.http.NameValuePair;
 import org.apache.http.client.utils.URLEncodedUtils;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
 
-import com.fasterxml.jackson.annotation.JsonProperty;
-import com.google.common.collect.Lists;
+import static com.google.common.collect.Ordering.from;
+import static java.lang.String.format;
 
 /**
  * {@link State} that contains information about an arbitrary number of torrent
@@ -45,11 +58,13 @@ public class TorrentState extends AbstractState implements Iterable<TorrentFile>
        @JsonProperty
        private List<TorrentFile> files = Lists.newArrayList();
 
+       private final Set<TorrentFile> newTorrentFiles = new HashSet<>();
+
        /**
         * Creates a new torrent state without torrent files.
         */
        public TorrentState() {
-               this(Collections.<TorrentFile> emptySet());
+               this(Collections.<TorrentFile>emptySet());
        }
 
        /**
@@ -62,6 +77,11 @@ public class TorrentState extends AbstractState implements Iterable<TorrentFile>
                files.addAll(torrentFiles);
        }
 
+       public TorrentState(Collection<TorrentFile> torrentFiles, Collection<TorrentFile> newTorrentFiles) {
+               files.addAll(torrentFiles);
+               this.newTorrentFiles.addAll(newTorrentFiles);
+       }
+
        //
        // ACCESSORS
        //
@@ -92,6 +112,90 @@ public class TorrentState extends AbstractState implements Iterable<TorrentFile>
                return this;
        }
 
+       @Nonnull
+       @Override
+       protected String summary(Reaction reaction) {
+               return format("Found %d new Torrent(s) for “%s!”", newTorrentFiles.size(), reaction.name());
+       }
+
+       @Nonnull
+       @Override
+       protected String plainText() {
+               StringBuilder plainText = new StringBuilder();
+               plainText.append("New Torrents:\n\n");
+               for (TorrentFile torrentFile : newTorrentFiles) {
+                       plainText.append(torrentFile.name()).append('\n');
+                       plainText.append('\t').append(torrentFile.size()).append(" in ").append(torrentFile.fileCount()).append(" file(s)\n");
+                       plainText.append('\t').append(torrentFile.seedCount()).append(" seed(s), ").append(torrentFile.leechCount()).append(" leecher(s)\n");
+                       if ((torrentFile.magnetUri() != null) && (torrentFile.magnetUri().length() > 0)) {
+                               plainText.append('\t').append(torrentFile.magnetUri()).append('\n');
+                       }
+                       if ((torrentFile.downloadUri() != null) && (torrentFile.downloadUri().length() > 0)) {
+                               plainText.append('\t').append(torrentFile.downloadUri()).append('\n');
+                       }
+                       plainText.append('\n');
+               }
+               return plainText.toString();
+       }
+
+       @Nullable
+       @Override
+       protected String htmlText() {
+               StringBuilder htmlBuilder = new StringBuilder();
+               htmlBuilder.append("<html><body>\n");
+               htmlBuilder.append("<table>\n<caption>All Known Torrents</caption>\n");
+               htmlBuilder.append("<thead>\n");
+               htmlBuilder.append("<tr>");
+               htmlBuilder.append("<th>Filename</th>");
+               htmlBuilder.append("<th>Size</th>");
+               htmlBuilder.append("<th>File(s)</th>");
+               htmlBuilder.append("<th>Seeds</th>");
+               htmlBuilder.append("<th>Leechers</th>");
+               htmlBuilder.append("<th>Magnet</th>");
+               htmlBuilder.append("<th>Download</th>");
+               htmlBuilder.append("</tr>\n");
+               htmlBuilder.append("</thead>\n");
+               htmlBuilder.append("<tbody>\n");
+               for (TorrentFile torrentFile : sortNewFirst().sortedCopy(files)) {
+                       if (newTorrentFiles.contains(torrentFile)) {
+                               htmlBuilder.append("<tr style=\"color: #008000; font-weight: bold;\">");
+                       } else {
+                               htmlBuilder.append("<tr>");
+                       }
+                       htmlBuilder.append("<td>").append(StringEscapeUtils.escapeHtml4(torrentFile.name())).append("</td>");
+                       htmlBuilder.append("<td>").append(StringEscapeUtils.escapeHtml4(torrentFile.size())).append("</td>");
+                       htmlBuilder.append("<td>").append(torrentFile.fileCount()).append("</td>");
+                       htmlBuilder.append("<td>").append(torrentFile.seedCount()).append("</td>");
+                       htmlBuilder.append("<td>").append(torrentFile.leechCount()).append("</td>");
+                       htmlBuilder.append("<td><a href=\"").append(StringEscapeUtils.escapeHtml4(torrentFile.magnetUri())).append("\">Link</a></td>");
+                       htmlBuilder.append("<td><a href=\"").append(StringEscapeUtils.escapeHtml4(torrentFile.downloadUri())).append("\">Link</a></td>");
+                       htmlBuilder.append("</tr>\n");
+               }
+               htmlBuilder.append("</tbody>\n");
+               htmlBuilder.append("</table>\n");
+               htmlBuilder.append("</body></html>\n");
+               return htmlBuilder.toString();
+       }
+
+       /**
+        * Returns an ordering that sorts torrent files by whether they are new
+        * (according to {@link #files}) or not. New files will be sorted
+        * first.
+        *
+        * @return An ordering for “new files first”
+        */
+       private Ordering<TorrentFile> sortNewFirst() {
+               return from((TorrentFile leftTorrentFile, TorrentFile rightTorrentFile) -> {
+                       if (newTorrentFiles.contains(leftTorrentFile) && !newTorrentFiles.contains(rightTorrentFile)) {
+                               return -1;
+                       }
+                       if (!newTorrentFiles.contains(leftTorrentFile) && newTorrentFiles.contains(rightTorrentFile)) {
+                               return 1;
+                       }
+                       return 0;
+               });
+       }
+
        //
        // ITERABLE METHODS
        //
@@ -113,7 +217,7 @@ public class TorrentState extends AbstractState implements Iterable<TorrentFile>
         */
        @Override
        public String toString() {
-               return String.format("%s[files=%s]", getClass().getSimpleName(), files);
+               return format("%s[files=%s]", getClass().getSimpleName(), files);
        }
 
        /**
@@ -332,7 +436,7 @@ public class TorrentState extends AbstractState implements Iterable<TorrentFile>
                 */
                @Override
                public String toString() {
-                       return String.format("%s(%s,%s,%s)", name(), size(), magnetUri(), downloadUri());
+                       return format("%s(%s,%s,%s)", name(), size(), magnetUri(), downloadUri());
                }
 
        }
index b916237..0018f50 100644 (file)
 
 package net.pterodactylus.rhynodge.triggers;
 
-import net.pterodactylus.rhynodge.Reaction;
 import net.pterodactylus.rhynodge.State;
 import net.pterodactylus.rhynodge.Trigger;
-import net.pterodactylus.rhynodge.output.DefaultOutput;
-import net.pterodactylus.rhynodge.output.Output;
-import net.pterodactylus.rhynodge.states.OutputState;
 
 /**
  * {@link Trigger} implementation that always triggers.
@@ -54,23 +50,4 @@ public class AlwaysTrigger implements Trigger {
                return true;
        }
 
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       public Output output(Reaction reaction) {
-               DefaultOutput output = new DefaultOutput(reaction.name());
-               if (currentState instanceof OutputState) {
-                       OutputState outputState = (OutputState) currentState;
-                       if (outputState.plainTextOutput().isPresent()) {
-                               output = output.addText("text/plain", outputState.plainTextOutput().get());
-                       }
-                       if (outputState.htmlOutput().isPresent()) {
-                               output = output.addText("text/html", outputState.htmlOutput().get());
-                       }
-                       return output;
-               }
-               return output.addText("text/plain", "true").addText("text/html", "<div>true</div>");
-       }
-
 }
index 264740f..8681871 100644 (file)
@@ -59,12 +59,4 @@ public class FileExistenceTrigger implements Trigger {
                return triggered;
        }
 
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       public Output output(Reaction reaction) {
-               return new DefaultOutput("File appeared/disappeared").addText("text/plain", "File appeared/disappeared").addText("text/html", "<div>File appeared/disappeared</div>");
-       }
-
 }
index 7b3026c..f1f114f 100644 (file)
@@ -58,12 +58,4 @@ public class FileStateModifiedTrigger implements Trigger {
                return triggered;
        }
 
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       public Output output(Reaction reaction) {
-               return new DefaultOutput("File modified").addText("text/plain", "File modified").addText("text/html", "<div>File modified</div>");
-       }
-
 }
index d2e0688..416b9e2 100644 (file)
 
 package net.pterodactylus.rhynodge.triggers;
 
-import static com.google.common.base.Preconditions.*;
+import java.util.HashSet;
+import java.util.Set;
 
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
-import net.pterodactylus.rhynodge.Reaction;
 import net.pterodactylus.rhynodge.State;
 import net.pterodactylus.rhynodge.Trigger;
-import net.pterodactylus.rhynodge.output.DefaultOutput;
-import net.pterodactylus.rhynodge.output.Output;
 import net.pterodactylus.rhynodge.states.ComicState;
 import net.pterodactylus.rhynodge.states.ComicState.Comic;
-import net.pterodactylus.rhynodge.states.ComicState.Strip;
 
-import com.google.common.collect.Lists;
-import org.apache.commons.lang3.StringEscapeUtils;
-import org.apache.commons.lang3.StringUtils;
+import static com.google.common.base.Preconditions.checkArgument;
 
 /**
  * {@link Trigger} implementation that detects the presence of new {@link
@@ -44,11 +35,7 @@ import org.apache.commons.lang3.StringUtils;
  */
 public class NewComicTrigger implements Trigger {
 
-       /** The new comics. */
-       private final List<Comic> newComics = Lists.newArrayList();
-
-       /** The latest comic state. */
-       private ComicState mergedComicState;
+       private boolean triggered = false;
 
        @Override
        public State mergeStates(State previousState, State currentState) {
@@ -58,110 +45,22 @@ public class NewComicTrigger implements Trigger {
                ComicState previousComicState = (ComicState) previousState;
                ComicState currentComicState = (ComicState) currentState;
 
-               /* copy old state into new state. */
-               mergedComicState = new ComicState();
-               for (Comic comic : previousComicState) {
-                       mergedComicState.add(comic);
-               }
+               Set<Comic> allComics = new HashSet<>(previousComicState.comics());
+               Set<Comic> newComics = new HashSet<>();
 
-               newComics.clear();
                for (Comic comic : currentComicState) {
-                       if (!mergedComicState.comics().contains(comic)) {
+                       if (allComics.add(comic)) {
                                newComics.add(comic);
-                               mergedComicState.add(comic);
+                               triggered = true;
                        }
                }
 
-               return mergedComicState;
+               return new ComicState(allComics, newComics);
        }
 
        @Override
        public boolean triggers() {
-               return !newComics.isEmpty();
-       }
-
-       @Override
-       public Output output(Reaction reaction) {
-               DefaultOutput output = new DefaultOutput(String.format("New Comic found for “%s!”", reaction.name()));
-
-               output.addText("text/plain", generatePlainText());
-               output.addText("text/html", generateHtmlText());
-
-               return output;
-       }
-
-       //
-       // PRIVATE METHODS
-       //
-
-       /**
-        * Generates a list of the new comics in plain text format.
-        *
-        * @return The list of new comics in plain text format
-        */
-       private String generatePlainText() {
-               StringBuilder text = new StringBuilder();
-
-               for (Comic newComic : newComics) {
-                       text.append("Comic Found: ").append(newComic.title()).append("\n\n");
-                       for (Strip strip : newComic) {
-                               text.append("Image: ").append(strip.imageUrl()).append("\n");
-                               if (!StringUtils.isBlank(strip.comment())) {
-                                       text.append("Comment: ").append(strip.comment()).append("\n");
-                               }
-                       }
-                       text.append("\n\n");
-               }
-
-               return text.toString();
-       }
-
-       /**
-        * Generates a list of new comics in HTML format.
-        *
-        * @return The list of new comics in HTML format
-        */
-       private String generateHtmlText() {
-               StringBuilder html = new StringBuilder();
-               html.append("<body>");
-
-               for (Comic newComic : newComics) {
-                       generateComicHtml(html, newComic);
-               }
-
-               List<Comic> latestComics = new ArrayList<Comic>(mergedComicState.comics());
-               Collections.reverse(latestComics);
-               int comicCount = 0;
-               for (Comic comic : latestComics) {
-                       if (newComics.contains(comic)) {
-                               continue;
-                       }
-                       generateComicHtml(html, comic);
-                       if (++comicCount == 7) {
-                               break;
-                       }
-               }
-
-               return html.append("</body>").toString();
-       }
-
-       /**
-        * Generates the HTML for a single comic.
-        *
-        * @param html
-        *              The string builder to append the HTML to
-        * @param comic
-        *              The comic to render
-        */
-       private void generateComicHtml(StringBuilder html, Comic comic) {
-               html.append("<h1>").append(StringEscapeUtils.escapeHtml4(comic.title())).append("</h1>\n");
-               for (Strip strip : comic) {
-                       html.append("<div><img src=\"").append(StringEscapeUtils.escapeHtml4(strip.imageUrl()));
-                       html.append("\" alt=\"").append(StringEscapeUtils.escapeHtml4(strip.comment()));
-                       html.append("\" title=\"").append(StringEscapeUtils.escapeHtml4(strip.comment()));
-                       html.append("\"></div>\n");
-                       html.append("<div>").append(StringEscapeUtils.escapeHtml4(strip.comment())).append("</div>\n");
-               }
+               return triggered;
        }
 
 }
index ea15c87..4b1d209 100644 (file)
 
 package net.pterodactylus.rhynodge.triggers;
 
-import static com.google.common.base.Preconditions.checkState;
-
+import java.util.ArrayList;
 import java.util.Collection;
+import java.util.HashSet;
 import java.util.Map;
-import java.util.Map.Entry;
 
-import net.pterodactylus.rhynodge.Reaction;
 import net.pterodactylus.rhynodge.State;
 import net.pterodactylus.rhynodge.Trigger;
-import net.pterodactylus.rhynodge.output.DefaultOutput;
-import net.pterodactylus.rhynodge.output.Output;
 import net.pterodactylus.rhynodge.states.EpisodeState;
 import net.pterodactylus.rhynodge.states.EpisodeState.Episode;
 import net.pterodactylus.rhynodge.states.TorrentState.TorrentFile;
 
-import org.apache.commons.lang3.StringEscapeUtils;
-
-import com.google.common.base.Function;
-import com.google.common.collect.FluentIterable;
-import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.Lists;
-import com.google.common.collect.Maps;
-import com.google.common.collect.Ordering;
-import com.google.common.collect.Sets;
+import static com.google.common.base.Preconditions.checkState;
+import static java.util.function.Function.identity;
+import static java.util.stream.Collectors.toMap;
 
 /**
  * {@link Trigger} implementation that compares two {@link EpisodeState}s for
@@ -50,21 +40,7 @@ import com.google.common.collect.Sets;
  */
 public class NewEpisodeTrigger implements Trigger {
 
-       /** All episodes. */
-       private final Collection<Episode> allEpisodes = Sets.newHashSet();
-
-       /** All new episodes. */
-       private final Collection<Episode> newEpisodes = Sets.newHashSet();
-
-       /** All changed episodes. */
-       private final Collection<Episode> changedEpisodes = Sets.newHashSet();
-
-       /** All new torrent files. */
-       private final Collection<TorrentFile> newTorrentFiles = Sets.newHashSet();
-
-       //
-       // TRIGGER METHODS
-       //
+       private boolean triggered = false;
 
        /**
         * {@inheritDoc}
@@ -73,24 +49,19 @@ public class NewEpisodeTrigger implements Trigger {
        public State mergeStates(State previousState, State currentState) {
                checkState(currentState instanceof EpisodeState, "currentState is not a EpisodeState but a %s", currentState.getClass().getName());
                checkState(previousState instanceof EpisodeState, "previousState is not a EpisodeState but a %s", currentState.getClass().getName());
-               newEpisodes.clear();
-               changedEpisodes.clear();
-               this.allEpisodes.clear();
-               newTorrentFiles.clear();
-               Map<Episode, Episode> allEpisodes = Maps.newHashMap(FluentIterable.from(((EpisodeState) previousState).episodes()).toMap(new Function<Episode, Episode>() {
 
-                       @Override
-                       public Episode apply(Episode episode) {
-                               return episode;
-                       }
-               }));
+               Collection<Episode> newEpisodes = new HashSet<>();
+               Collection<Episode> changedEpisodes = new HashSet<>();
+               Collection<TorrentFile> newTorrentFiles = new HashSet<>();
+               Map<Episode, Episode> allEpisodes = ((EpisodeState) previousState).episodes().stream().collect(toMap(identity(), identity()));
                for (Episode episode : ((EpisodeState) currentState).episodes()) {
                        if (!allEpisodes.containsKey(episode)) {
                                allEpisodes.put(episode, episode);
                                newEpisodes.add(episode);
+                               triggered = true;
                        }
                        Episode existingEpisode = allEpisodes.get(episode);
-                       for (TorrentFile torrentFile : Lists.newArrayList(episode.torrentFiles())) {
+                       for (TorrentFile torrentFile : new ArrayList<>(episode.torrentFiles())) {
                                int oldSize = existingEpisode.torrentFiles().size();
                                existingEpisode.addTorrentFile(torrentFile);
                                int newSize = existingEpisode.torrentFiles().size();
@@ -102,8 +73,7 @@ public class NewEpisodeTrigger implements Trigger {
                                }
                        }
                }
-               this.allEpisodes.addAll(allEpisodes.values());
-               return new EpisodeState(this.allEpisodes);
+               return new EpisodeState(allEpisodes.values(), newEpisodes, changedEpisodes, newTorrentFiles);
        }
 
        /**
@@ -111,155 +81,7 @@ public class NewEpisodeTrigger implements Trigger {
         */
        @Override
        public boolean triggers() {
-               return !newEpisodes.isEmpty() || !changedEpisodes.isEmpty();
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       public Output output(Reaction reaction) {
-               String summary;
-               if (!newEpisodes.isEmpty()) {
-                       if (!changedEpisodes.isEmpty()) {
-                               summary = String.format("%d new and %d changed Torrent(s) for “%s!”", newEpisodes.size(), changedEpisodes.size(), reaction.name());
-                       } else {
-                               summary = String.format("%d new Torrent(s) for “%s!”", newEpisodes.size(), reaction.name());
-                       }
-               } else {
-                       summary = String.format("%d changed Torrent(s) for “%s!”", changedEpisodes.size(), reaction.name());
-               }
-               DefaultOutput output = new DefaultOutput(summary);
-               output.addText("text/plain", generatePlainText(reaction));
-               output.addText("text/html", generateHtmlText(reaction));
-               return output;
-       }
-
-       //
-       // PRIVATE METHODS
-       //
-
-       /**
-        * Generates the plain text trigger output.
-        *
-        * @param reaction
-        *            The reaction that was triggered
-        * @return The plain text output
-        */
-       private String generatePlainText(Reaction reaction) {
-               StringBuilder stringBuilder = new StringBuilder();
-               if (!newEpisodes.isEmpty()) {
-                       stringBuilder.append(reaction.name()).append(" - New Episodes\n\n");
-                       for (Episode episode : newEpisodes) {
-                               stringBuilder.append("- ").append(episode.identifier()).append("\n");
-                               for (TorrentFile torrentFile : episode) {
-                                       stringBuilder.append("  - ").append(torrentFile.name()).append(", ").append(torrentFile.size()).append("\n");
-                                       if ((torrentFile.magnetUri() != null) && (torrentFile.magnetUri().length() > 0)) {
-                                               stringBuilder.append("    Magnet: ").append(torrentFile.magnetUri()).append("\n");
-                                       }
-                                       if ((torrentFile.downloadUri() != null) && (torrentFile.downloadUri().length() > 0)) {
-                                               stringBuilder.append("    Download: ").append(torrentFile.downloadUri()).append("\n");
-                                       }
-                               }
-                       }
-               }
-               if (!changedEpisodes.isEmpty()) {
-                       stringBuilder.append(reaction.name()).append(" - Changed Episodes\n\n");
-                       for (Episode episode : changedEpisodes) {
-                               stringBuilder.append("- ").append(episode.identifier()).append("\n");
-                               for (TorrentFile torrentFile : episode) {
-                                       stringBuilder.append("  - ").append(torrentFile.name()).append(", ").append(torrentFile.size()).append("\n");
-                                       if ((torrentFile.magnetUri() != null) && (torrentFile.magnetUri().length() > 0)) {
-                                               stringBuilder.append("    Magnet: ").append(torrentFile.magnetUri()).append("\n");
-                                       }
-                                       if ((torrentFile.downloadUri() != null) && (torrentFile.downloadUri().length() > 0)) {
-                                               stringBuilder.append("    Download: ").append(torrentFile.downloadUri()).append("\n");
-                                       }
-                               }
-                       }
-               }
-               /* list all known episodes. */
-               stringBuilder.append(reaction.name()).append(" - All Known Episodes\n\n");
-               ImmutableMap<Integer, Collection<Episode>> episodesBySeason = FluentIterable.from(allEpisodes).index(new Function<Episode, Integer>() {
-
-                       @Override
-                       public Integer apply(Episode episode) {
-                               return episode.season();
-                       }
-               }).asMap();
-               for (Entry<Integer, Collection<Episode>> seasonEntry : episodesBySeason.entrySet()) {
-                       stringBuilder.append("  Season ").append(seasonEntry.getKey()).append("\n\n");
-                       for (Episode episode : Ordering.natural().sortedCopy(seasonEntry.getValue())) {
-                               stringBuilder.append("    Episode ").append(episode.episode()).append("\n");
-                               for (TorrentFile torrentFile : episode) {
-                                       stringBuilder.append("      Size: ").append(torrentFile.size());
-                                       stringBuilder.append(" in ").append(torrentFile.fileCount()).append(" file(s): ");
-                                       stringBuilder.append(torrentFile.magnetUri());
-                               }
-                       }
-               }
-
-               return stringBuilder.toString();
-       }
-
-       /**
-        * Generates the HTML trigger output.
-        *
-        * @param reaction
-        *            The reaction that was triggered
-        * @return The HTML output
-        */
-       private String generateHtmlText(Reaction reaction) {
-               StringBuilder htmlBuilder = new StringBuilder();
-               htmlBuilder.append("<html><body>\n");
-               /* show all known episodes. */
-               htmlBuilder.append("<table>\n<caption>All Known Episodes</caption>\n");
-               htmlBuilder.append("<thead>\n");
-               htmlBuilder.append("<tr>");
-               htmlBuilder.append("<th>Season</th>");
-               htmlBuilder.append("<th>Episode</th>");
-               htmlBuilder.append("<th>Filename</th>");
-               htmlBuilder.append("<th>Size</th>");
-               htmlBuilder.append("<th>File(s)</th>");
-               htmlBuilder.append("<th>Seeds</th>");
-               htmlBuilder.append("<th>Leechers</th>");
-               htmlBuilder.append("<th>Magnet</th>");
-               htmlBuilder.append("<th>Download</th>");
-               htmlBuilder.append("</tr>\n");
-               htmlBuilder.append("</thead>\n");
-               htmlBuilder.append("<tbody>\n");
-               Episode lastEpisode = null;
-               for (Entry<Integer, Collection<Episode>> seasonEntry : FluentIterable.from(Ordering.natural().reverse().sortedCopy(allEpisodes)).index(Episode.BY_SEASON).asMap().entrySet()) {
-                       for (Episode episode : seasonEntry.getValue()) {
-                               for (TorrentFile torrentFile : episode) {
-                                       if (newEpisodes.contains(episode)) {
-                                               htmlBuilder.append("<tr style=\"color: #008000; font-weight: bold;\">");
-                                       } else if (newTorrentFiles.contains(torrentFile)) {
-                                               htmlBuilder.append("<tr style=\"color: #008000;\">");
-                                       } else {
-                                               htmlBuilder.append("<tr>");
-                                       }
-                                       if ((lastEpisode == null) || !lastEpisode.equals(episode)) {
-                                               htmlBuilder.append("<td>").append(episode.season()).append("</td><td>").append(episode.episode()).append("</td>");
-                                       } else {
-                                               htmlBuilder.append("<td colspan=\"2\"></td>");
-                                       }
-                                       htmlBuilder.append("<td>").append(StringEscapeUtils.escapeHtml4(torrentFile.name())).append("</td>");
-                                       htmlBuilder.append("<td>").append(StringEscapeUtils.escapeHtml4(torrentFile.size())).append("</td>");
-                                       htmlBuilder.append("<td>").append(torrentFile.fileCount()).append("</td>");
-                                       htmlBuilder.append("<td>").append(torrentFile.seedCount()).append("</td>");
-                                       htmlBuilder.append("<td>").append(torrentFile.leechCount()).append("</td>");
-                                       htmlBuilder.append("<td><a href=\"").append(StringEscapeUtils.escapeHtml4(torrentFile.magnetUri())).append("\">Link</a></td>");
-                                       htmlBuilder.append("<td><a href=\"").append(StringEscapeUtils.escapeHtml4(torrentFile.downloadUri())).append("\">Link</a></td>");
-                                       htmlBuilder.append("</tr>\n");
-                                       lastEpisode = episode;
-                               }
-                       }
-               }
-               htmlBuilder.append("</tbody>\n");
-               htmlBuilder.append("</table>\n");
-               htmlBuilder.append("</body></html>\n");
-               return htmlBuilder.toString();
+               return triggered;
        }
 
 }
index 7b28fee..0ff193c 100644 (file)
 
 package net.pterodactylus.rhynodge.triggers;
 
-import static com.google.common.base.Preconditions.checkState;
-
-import java.util.List;
+import java.util.HashSet;
 import java.util.Set;
 
-import net.pterodactylus.rhynodge.Reaction;
 import net.pterodactylus.rhynodge.State;
 import net.pterodactylus.rhynodge.Trigger;
-import net.pterodactylus.rhynodge.output.DefaultOutput;
-import net.pterodactylus.rhynodge.output.Output;
 import net.pterodactylus.rhynodge.states.TorrentState;
 import net.pterodactylus.rhynodge.states.TorrentState.TorrentFile;
 
-import org.apache.commons.lang3.StringEscapeUtils;
-
-import com.google.common.collect.Lists;
-import com.google.common.collect.Ordering;
-import com.google.common.collect.Sets;
+import static com.google.common.base.Preconditions.checkState;
 
 /**
  * {@link Trigger} implementation that is triggered by {@link TorrentFile}s that
@@ -44,11 +35,7 @@ import com.google.common.collect.Sets;
  */
 public class NewTorrentTrigger implements Trigger {
 
-       /** All known torrents. */
-       private final Set<TorrentFile> allTorrentFiles = Sets.newHashSet();
-
-       /** The newly detected torrent files. */
-       private final List<TorrentFile> newTorrentFiles = Lists.newArrayList();
+       private boolean triggered = false;
 
        //
        // TRIGGER METHODS
@@ -61,19 +48,17 @@ public class NewTorrentTrigger implements Trigger {
        public State mergeStates(State previousState, State currentState) {
                checkState(currentState instanceof TorrentState, "currentState is not a TorrentState but a %s", currentState.getClass().getName());
                checkState(previousState instanceof TorrentState, "previousState is not a TorrentState but a %s", currentState.getClass().getName());
-               TorrentState currentTorrentState = (TorrentState) currentState;
-               TorrentState previousTorrentState = (TorrentState) previousState;
 
-               allTorrentFiles.clear();
-               newTorrentFiles.clear();
-               allTorrentFiles.addAll(previousTorrentState.torrentFiles());
-               for (TorrentFile torrentFile : currentTorrentState) {
+               Set<TorrentFile> allTorrentFiles = new HashSet<>(((TorrentState) previousState).torrentFiles());
+               Set<TorrentFile> newTorrentFiles = new HashSet<>();
+               for (TorrentFile torrentFile : (TorrentState) currentState) {
                        if (allTorrentFiles.add(torrentFile)) {
                                newTorrentFiles.add(torrentFile);
+                               triggered = true;
                        }
                }
 
-               return new TorrentState(allTorrentFiles);
+               return new TorrentState(allTorrentFiles, newTorrentFiles);
        }
 
        /**
@@ -81,114 +66,7 @@ public class NewTorrentTrigger implements Trigger {
         */
        @Override
        public boolean triggers() {
-               return !newTorrentFiles.isEmpty();
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       public Output output(Reaction reaction) {
-               DefaultOutput output = new DefaultOutput(String.format("Found %d new Torrent(s) for “%s!”", newTorrentFiles.size(), reaction.name()));
-               output.addText("text/plain", getPlainTextList(reaction));
-               output.addText("text/html", getHtmlTextList(reaction));
-               return output;
-       }
-
-       //
-       // PRIVATE METHODS
-       //
-
-       /**
-        * Generates a plain text list of torrent files.
-        *
-        * @param reaction
-        *            The reaction that was triggered
-        * @return The generated plain text
-        */
-       private String getPlainTextList(Reaction reaction) {
-               StringBuilder plainText = new StringBuilder();
-               plainText.append("New Torrents:\n\n");
-               for (TorrentFile torrentFile : newTorrentFiles) {
-                       plainText.append(torrentFile.name()).append('\n');
-                       plainText.append('\t').append(torrentFile.size()).append(" in ").append(torrentFile.fileCount()).append(" file(s)\n");
-                       plainText.append('\t').append(torrentFile.seedCount()).append(" seed(s), ").append(torrentFile.leechCount()).append(" leecher(s)\n");
-                       if ((torrentFile.magnetUri() != null) && (torrentFile.magnetUri().length() > 0)) {
-                               plainText.append('\t').append(torrentFile.magnetUri()).append('\n');
-                       }
-                       if ((torrentFile.downloadUri() != null) && (torrentFile.downloadUri().length() > 0)) {
-                               plainText.append('\t').append(torrentFile.downloadUri()).append('\n');
-                       }
-                       plainText.append('\n');
-               }
-               return plainText.toString();
-       }
-
-       /**
-        * Generates an HTML list of the given torrent files.
-        *
-        * @param reaction
-        *            The reaction that was triggered
-        * @return The generated HTML
-        */
-       private String getHtmlTextList(Reaction reaction) {
-               StringBuilder htmlBuilder = new StringBuilder();
-               htmlBuilder.append("<html><body>\n");
-               htmlBuilder.append("<table>\n<caption>All Known Torrents</caption>\n");
-               htmlBuilder.append("<thead>\n");
-               htmlBuilder.append("<tr>");
-               htmlBuilder.append("<th>Filename</th>");
-               htmlBuilder.append("<th>Size</th>");
-               htmlBuilder.append("<th>File(s)</th>");
-               htmlBuilder.append("<th>Seeds</th>");
-               htmlBuilder.append("<th>Leechers</th>");
-               htmlBuilder.append("<th>Magnet</th>");
-               htmlBuilder.append("<th>Download</th>");
-               htmlBuilder.append("</tr>\n");
-               htmlBuilder.append("</thead>\n");
-               htmlBuilder.append("<tbody>\n");
-               for (TorrentFile torrentFile : sortNewFirst().sortedCopy(allTorrentFiles)) {
-                       if (newTorrentFiles.contains(torrentFile)) {
-                               htmlBuilder.append("<tr style=\"color: #008000; font-weight: bold;\">");
-                       } else {
-                               htmlBuilder.append("<tr>");
-                       }
-                       htmlBuilder.append("<td>").append(StringEscapeUtils.escapeHtml4(torrentFile.name())).append("</td>");
-                       htmlBuilder.append("<td>").append(StringEscapeUtils.escapeHtml4(torrentFile.size())).append("</td>");
-                       htmlBuilder.append("<td>").append(torrentFile.fileCount()).append("</td>");
-                       htmlBuilder.append("<td>").append(torrentFile.seedCount()).append("</td>");
-                       htmlBuilder.append("<td>").append(torrentFile.leechCount()).append("</td>");
-                       htmlBuilder.append("<td><a href=\"").append(StringEscapeUtils.escapeHtml4(torrentFile.magnetUri())).append("\">Link</a></td>");
-                       htmlBuilder.append("<td><a href=\"").append(StringEscapeUtils.escapeHtml4(torrentFile.downloadUri())).append("\">Link</a></td>");
-                       htmlBuilder.append("</tr>\n");
-               }
-               htmlBuilder.append("</tbody>\n");
-               htmlBuilder.append("</table>\n");
-               htmlBuilder.append("</body></html>\n");
-               return htmlBuilder.toString();
-       }
-
-       /**
-        * Returns an ordering that sorts torrent files by whether they are new
-        * (according to {@link #newTorrentFiles}) or not. New files will be sorted
-        * first.
-        *
-        * @return An ordering for “new files first”
-        */
-       private Ordering<TorrentFile> sortNewFirst() {
-               return new Ordering<TorrentFile>() {
-
-                       @Override
-                       public int compare(TorrentFile leftTorrentFile, TorrentFile rightTorrentFile) {
-                               if (newTorrentFiles.contains(leftTorrentFile) && !newTorrentFiles.contains(rightTorrentFile)) {
-                                       return -1;
-                               }
-                               if (!newTorrentFiles.contains(leftTorrentFile) && newTorrentFiles.contains(rightTorrentFile)) {
-                                       return 1;
-                               }
-                               return 0;
-                       }
-               };
+               return triggered;
        }
 
 }
index 0d1d7f1..814af33 100644 (file)
@@ -2,10 +2,22 @@ package net.pterodactylus.rhynodge.webpages.weather
 
 import com.fasterxml.jackson.annotation.JsonGetter
 import com.fasterxml.jackson.annotation.JsonProperty
+import kotlinx.html.body
+import kotlinx.html.div
+import kotlinx.html.h1
+import kotlinx.html.head
+import kotlinx.html.html
+import kotlinx.html.img
+import kotlinx.html.stream.createHTML
+import kotlinx.html.style
+import kotlinx.html.unsafe
 import net.pterodactylus.rhynodge.states.AbstractState
+import java.text.DateFormat
 import java.time.Instant
 import java.time.ZoneId
 import java.time.ZonedDateTime
+import java.time.temporal.ChronoUnit
+import java.util.Locale
 
 /**
  * Contains a weather state.
@@ -14,26 +26,97 @@ import java.time.ZonedDateTime
  */
 class WeatherState(val service: String, val dateTime: ZonedDateTime) : AbstractState(true), Iterable<HourState> {
 
-    constructor(@JsonProperty("service") service: String, @JsonProperty("dateTime") time: Long) :
-    this(service, Instant.ofEpochMilli(time).atZone(ZoneId.of("Europe/Berlin")))
+       constructor(@JsonProperty("service") service: String, @JsonProperty("dateTime") time: Long) :
+                       this(service, Instant.ofEpochMilli(time).atZone(ZoneId.of("Europe/Berlin")))
 
-    @JsonProperty("hours")
-    val hours: List<HourState> = mutableListOf()
+       @JsonProperty("hours")
+       val hours: List<HourState> = mutableListOf()
 
-    @get:JsonGetter("dateTime")
-    val timeMillis = dateTime.toInstant().toEpochMilli()
+       @get:JsonGetter("dateTime")
+       val timeMillis = dateTime.toInstant().toEpochMilli()
 
-    operator fun plusAssign(hourState: HourState) {
-        (hours as MutableList<HourState>).add(hourState)
-    }
+       operator fun plusAssign(hourState: HourState) {
+               (hours as MutableList<HourState>).add(hourState)
+       }
 
-    override fun iterator(): Iterator<HourState> {
-        return hours.iterator()
-    }
+       override fun iterator(): Iterator<HourState> {
+               return hours.iterator()
+       }
 
-    override fun equals(other: Any?): Boolean {
-        other as? WeatherState ?: return false
-        return (dateTime == other.dateTime) and (hours == other.hours)
-    }
+       override fun equals(other: Any?): Boolean {
+               other as? WeatherState ?: return false
+               return (dateTime == other.dateTime) and (hours == other.hours)
+       }
+
+       override fun plainText(): String=""
+
+       override fun htmlText(): String =
+                       createHTML().html {
+                               head {
+                                       style("text/css") {
+                                               unsafe {
+                                                       +".weather-states { display: table; } "
+                                                       +".hour-state, .header { display: table-row; } "
+                                                       +".hour-state > div { display: table-cell; padding: 0em 0.5em; text-align: center; } "
+                                                       +".header > div { display: table-cell; padding: 0em 0.5em; font-weight: bold; text-align: center; } "
+                                               }
+                                       }
+                               }
+                               body {
+                                       val startTime = dateTime.toInstant()
+                                       h1 { +"The Weather (according to wetter.de) on %s".format(dateFormatter.format(startTime.toEpochMilli())) }
+                                       val showFeltTemperature = any { it.feltTemperature != null }
+                                       val showGustSpeed = any { it.gustSpeed != null }
+                                       val showHumidity = any { it.humidity != null }
+                                       div("weather-states") {
+                                               div("header") {
+                                                       div { +"Time" }
+                                                       div { +"Temperature" }
+                                                       if (showHumidity) {
+                                                               div { +"feels like" }
+                                                       }
+                                                       div { +"Chance of Rain" }
+                                                       div { +"Amount" }
+                                                       div { +"Wind from" }
+                                                       div { +"Speed" }
+                                                       if (showGustSpeed) {
+                                                               div { +"Gusts" }
+                                                       }
+                                                       if (showHumidity) {
+                                                               div { +"Humidity" }
+                                                       }
+                                                       div { +"Description" }
+                                                       div { +"Image" }
+                                               }
+                                               forEach {
+                                                       div("hour-state") {
+                                                               div("time") { +"%tH:%<tM".format(startTime.plus(it.hourIndex.toLong(), ChronoUnit.HOURS).toEpochMilli()) }
+                                                               div("temperature") { +"${it.temperature} °C" }
+                                                               if (showFeltTemperature) {
+                                                                       div("felt-temperature") { +if (it.feltTemperature != null) "(${it.feltTemperature} °C)" else "" }
+                                                               }
+                                                               div("rain-probability") { +"${it.rainProbability.times(100).toInt()}%" }
+                                                               div("rain-amount") { +"${it.rainAmount.minDigits()} l/m²" }
+                                                               div("wind-direction") { +it.windDirection.arrow }
+                                                               div("wind-speed") { +"${it.windSpeed} km/h" }
+                                                               if (showGustSpeed) {
+                                                                       div("gust-speed") { +if (it.gustSpeed != null) "(up to ${it.gustSpeed} km/h)" else "" }
+                                                               }
+                                                               if (showHumidity) {
+                                                                       div("humidity") { +if (it.humidity != null) "${it.humidity.times(100).toInt()}%" else "" }
+                                                               }
+                                                               div("description") { +it.description }
+                                                               div("image") { img(src = it.image) }
+                                                       }
+                                               }
+                                       }
+                               }
+                       }.toString()
 
 }
+
+private val dateFormatter: DateFormat =
+        DateFormat.getDateInstance(DateFormat.LONG, Locale.ENGLISH)
+
+private fun Double.minDigits(): String =
+               toString().replace(Regex("\\.0*$"), "")
index 5374d4c..bdc347a 100644 (file)
@@ -1,22 +1,7 @@
 package net.pterodactylus.rhynodge.webpages.weather
 
-import kotlinx.html.body
-import kotlinx.html.div
-import kotlinx.html.h1
-import kotlinx.html.head
-import kotlinx.html.html
-import kotlinx.html.img
-import kotlinx.html.stream.createHTML
-import kotlinx.html.style
-import kotlinx.html.unsafe
-import net.pterodactylus.rhynodge.Reaction
 import net.pterodactylus.rhynodge.State
 import net.pterodactylus.rhynodge.Trigger
-import net.pterodactylus.rhynodge.output.DefaultOutput
-import net.pterodactylus.rhynodge.output.Output
-import java.text.DateFormat
-import java.time.temporal.ChronoUnit
-import java.util.Locale
 
 /**
  * Detects changes in the weather and creates email texts.
@@ -25,92 +10,15 @@ import java.util.Locale
  */
 class WeatherTrigger : Trigger {
 
-    private val dateFormatter = DateFormat.getDateInstance(DateFormat.LONG, Locale.ENGLISH)
-    private lateinit var state: WeatherState
-    private var changed = false
+       private lateinit var state: WeatherState
+       private var changed = false
 
-    override fun mergeStates(previousState: State, currentState: State): State {
-        changed = previousState != currentState
-        state = currentState as WeatherState
-        return currentState
-    }
+       override fun mergeStates(previousState: State, currentState: State): State {
+               changed = previousState != currentState
+               state = currentState as WeatherState
+               return currentState
+       }
 
-    override fun triggers(): Boolean {
-        return changed
-    }
-
-    override fun output(reaction: Reaction): Output {
-        val output = DefaultOutput("The Weather (according to ${state.service}) on ${dateFormatter.format(state.dateTime.toInstant().toEpochMilli())}")
-        output.addText("text/html", generateHtmlOutput())
-        return output
-    }
-
-    private fun generateHtmlOutput(): String {
-        return createHTML().html {
-            head {
-                style("text/css") {
-                    unsafe {
-                        +".weather-states { display: table; } "
-                        +".hour-state, .header { display: table-row; } "
-                        +".hour-state > div { display: table-cell; padding: 0em 0.5em; text-align: center; } "
-                        +".header > div { display: table-cell; padding: 0em 0.5em; font-weight: bold; text-align: center; } "
-                    }
-                }
-            }
-            body {
-                val startTime = state.dateTime.toInstant()
-                h1 { +"The Weather (according to wetter.de) on %s".format(dateFormatter.format(startTime.toEpochMilli())) }
-                val showFeltTemperature = state.any { it.feltTemperature != null }
-                val showGustSpeed = state.any { it.gustSpeed != null }
-                val showHumidity = state.any { it.humidity != null }
-                div("weather-states") {
-                    div("header") {
-                        div { +"Time" }
-                        div { +"Temperature" }
-                        if (showHumidity) {
-                            div { +"feels like" }
-                        }
-                        div { +"Chance of Rain" }
-                        div { +"Amount" }
-                        div { +"Wind from" }
-                        div { +"Speed" }
-                        if (showGustSpeed) {
-                            div { +"Gusts" }
-                        }
-                        if (showHumidity) {
-                            div { +"Humidity" }
-                        }
-                        div { +"Description" }
-                        div { +"Image" }
-                    }
-                    state.forEach {
-                        div("hour-state") {
-                            div("time") { +"%tH:%<tM".format(startTime.plus(it.hourIndex.toLong(), ChronoUnit.HOURS).toEpochMilli()) }
-                            div("temperature") { +"${it.temperature} °C" }
-                            if (showFeltTemperature) {
-                                div("felt-temperature") { +if (it.feltTemperature != null) "(${it.feltTemperature} °C)" else "" }
-                            }
-                            div("rain-probability") { +"${it.rainProbability.times(100).toInt()}%" }
-                            div("rain-amount") { +"${it.rainAmount.minDigits()} l/m²" }
-                            div("wind-direction") { +it.windDirection.arrow }
-                            div("wind-speed") { +"${it.windSpeed} km/h" }
-                            if (showGustSpeed) {
-                                div("gust-speed") { +if (it.gustSpeed != null) "(up to ${it.gustSpeed} km/h)" else "" }
-                            }
-                            if (showHumidity) {
-                                div("humidity") { +if (it.humidity != null) "${it.humidity.times(100).toInt()}%" else "" }
-                            }
-                            div("description") { +it.description }
-                            div("image") { img(src = it.image) }
-                        }
-                    }
-                }
-            }
-        }.toString()
-    }
-
-    private fun Double.minDigits(): String {
-        return this.toString().replace(Regex("\\.0*$"), "")
-    }
+       override fun triggers() = changed
 
 }
index 839b389..8f250ab 100644 (file)
@@ -1,15 +1,9 @@
 package net.pterodactylus.rhynodge.states;
 
-import static com.google.common.base.Charsets.UTF_8;
-import static com.google.common.base.Objects.equal;
-import static org.apache.log4j.Level.OFF;
-import static org.apache.log4j.Logger.getLogger;
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.is;
-
 import java.io.File;
 import java.io.IOException;
 import java.util.Optional;
+import javax.annotation.Nonnull;
 
 import net.pterodactylus.rhynodge.State;
 import net.pterodactylus.rhynodge.states.StateManager.StateDirectory;
@@ -22,6 +16,13 @@ import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.TemporaryFolder;
 
+import static com.google.common.base.Charsets.UTF_8;
+import static com.google.common.base.Objects.equal;
+import static org.apache.log4j.Level.OFF;
+import static org.apache.log4j.Logger.getLogger;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+
 /**
  * Unit test for {@link StateManager}.
  *
@@ -107,6 +108,12 @@ public class StateManagerTest {
                                        && (time() == testState.time());
                }
 
+               @Nonnull
+               @Override
+               protected String plainText() {
+                       return "Test";
+               }
+
        }
 
        public static class InvalidState extends AbstractState {
@@ -114,6 +121,12 @@ public class StateManagerTest {
                @JsonProperty
                private final Object someObject = new Object();
 
+               @Nonnull
+               @Override
+               protected String plainText() {
+                       return "Invalid";
+               }
+
        }
 
 }
index 209454d..dd2a3c1 100644 (file)
@@ -4,6 +4,8 @@ import net.pterodactylus.rhynodge.Query
 import net.pterodactylus.rhynodge.State
 import net.pterodactylus.rhynodge.states.AbstractState
 import net.pterodactylus.rhynodge.states.FailedState
+import net.pterodactylus.rhynodge.states.StateManagerTest
+import net.pterodactylus.rhynodge.states.StateManagerTest.TestState
 import org.hamcrest.MatcherAssert.assertThat
 import org.hamcrest.Matchers.`is`
 import org.hamcrest.Matchers.sameInstance
@@ -37,14 +39,12 @@ class FallbackQueryTest {
 
        @Test
        fun `fallback query returns state of third query`() {
-               val successState: AbstractState = object : AbstractState() {}
                setupQueries(thirdState = successState)
                assertThat(query.state(), sameInstance<State>(successState))
        }
 
        @Test
        fun `fallback query calls all three queries`() {
-               val successState: AbstractState = object : AbstractState() {}
                setupQueries(thirdState = successState)
                query.state()
                verify(firstQuery).state()
@@ -54,14 +54,12 @@ class FallbackQueryTest {
 
        @Test
        fun `fallback query returns second state`() {
-               val successState: AbstractState = object : AbstractState() {}
                setupQueries(secondState = successState)
                assertThat(query.state(), sameInstance<State>(successState))
        }
 
        @Test
        fun `fallback query does not query third query`() {
-               val successState: AbstractState = object : AbstractState() {}
                setupQueries(secondState = successState)
                query.state()
                verify(firstQuery).state()
@@ -71,14 +69,12 @@ class FallbackQueryTest {
 
        @Test
        fun `fallback query returns first state`() {
-               val successState: AbstractState = object : AbstractState() {}
                setupQueries(firstState = successState)
                assertThat(query.state(), sameInstance<State>(successState))
        }
 
        @Test
        fun `fallback query does not query second and third query`() {
-               val successState: AbstractState = object : AbstractState() {}
                setupQueries(firstState = successState)
                query.state()
                verify(firstQuery).state()
@@ -99,3 +95,5 @@ class FallbackQueryTest {
        }
 
 }
+
+private val successState: AbstractState = TestState()
index 0a85bea..6945a0e 100644 (file)
@@ -1,11 +1,8 @@
 package net.pterodactylus.rhynodge.triggers
 
-import net.pterodactylus.rhynodge.Reaction
 import net.pterodactylus.rhynodge.State
 import net.pterodactylus.rhynodge.states.FailedState
 import net.pterodactylus.rhynodge.states.StateManagerTest.TestState
-import net.pterodactylus.rhynodge.testAction
-import net.pterodactylus.rhynodge.testQuery
 import org.hamcrest.MatcherAssert.assertThat
 import org.hamcrest.Matchers.equalTo
 import org.hamcrest.Matchers.sameInstance
@@ -30,21 +27,6 @@ class AlwaysTriggerTest {
                assertThat(trigger.triggers(), equalTo(true))
        }
 
-       @Test
-       @Suppress("NonAsciiCharacters")
-       fun `output returns “true” for plain text`() {
-               trigger.mergeStates(previousState, successfulState)
-               val output = trigger.output(Reaction("Test", testQuery(), trigger, testAction()))
-               assertThat(output.text("text/plain"), equalTo("true"))
-       }
-
-       @Test
-       fun `output returns true in a div for html`() {
-               trigger.mergeStates(previousState, successfulState)
-               val output = trigger.output(Reaction("Test", testQuery(), trigger, testAction()))
-               assertThat(output.text("text/html"), equalTo("<div>true</div>"))
-       }
-
        private val trigger = AlwaysTrigger()
        private val previousState = TestState()
        private val successfulState: State = TestState()
index ca140c0..c4063cb 100644 (file)
@@ -1,14 +1,9 @@
 package net.pterodactylus.rhynodge.webpages.weather
 
-import net.pterodactylus.rhynodge.Reaction
 import net.pterodactylus.rhynodge.State
 import org.hamcrest.MatcherAssert.assertThat
 import org.hamcrest.Matchers.`is`
-import org.hamcrest.Matchers.containsString
-import org.hamcrest.Matchers.not
 import org.junit.Test
-import org.mockito.Mockito.mock
-import java.io.File
 import java.time.ZoneId.of
 import java.time.ZonedDateTime
 
@@ -43,91 +38,4 @@ class WeatherTriggerTest {
         assertThat(trigger.triggers(), `is`(true))
     }
 
-    @Test
-    fun outputContainsCorrectSummary() {
-        val currentState = WeatherState("Weather", ZonedDateTime.of(2016, 5, 28, 0, 0, 0, 0, of("Europe/Berlin")))
-        trigger.mergeStates(previousState, currentState)
-        val reaction = mock(Reaction::class.java)
-        val output = trigger.output(reaction)
-        assertThat(output.summary(), `is`("The Weather (according to Weather) on May 28, 2016"))
-    }
-
-    @Test
-    fun outputContainsCorrectHourData() {
-        val currentState = WeatherState("Weather", ZonedDateTime.of(2016, 5, 28, 0, 0, 0, 0, of("Europe/Berlin")))
-        currentState += HourState(0, 10, 11, 0.12, 13.0, WindDirection.SOUTHSOUTHEAST, 14, 15, 0.16, "17", "http://18")
-        currentState += HourState(1, 20, 21, 0.22, 23.0, WindDirection.NORTHNORTHWEST, 24, 25, 0.26, "27", "http://28")
-        trigger.mergeStates(previousState, currentState)
-        val reaction = mock(Reaction::class.java)
-        val output = trigger.output(reaction)
-        val htmlText = output.text("text/html")
-        File("/tmp/wetter.html").writer().use { it.write(htmlText) }
-        assertThat(htmlText, containsString("00:00"))
-        assertThat(htmlText, containsString("10 °C"))
-        assertThat(htmlText, containsString("(11 °C)"))
-        assertThat(htmlText, containsString("12%"))
-        assertThat(htmlText, containsString("13 l/m²"))
-        assertThat(htmlText, containsString("↖↑"))
-        assertThat(htmlText, containsString("14 km/h"))
-        assertThat(htmlText, containsString("15 km/h"))
-        assertThat(htmlText, containsString("16%"))
-        assertThat(htmlText, containsString("17"))
-        assertThat(htmlText, containsString("http://18"))
-        assertThat(htmlText, containsString("01:00"))
-        assertThat(htmlText, containsString("20 °C"))
-        assertThat(htmlText, containsString("(21 °C)"))
-        assertThat(htmlText, containsString("22%"))
-        assertThat(htmlText, containsString("23 l/m²"))
-        assertThat(htmlText, containsString("↘↓"))
-        assertThat(htmlText, containsString("24 km/h"))
-        assertThat(htmlText, containsString("25 km/h"))
-        assertThat(htmlText, containsString("26%"))
-        assertThat(htmlText, containsString("27"))
-        assertThat(htmlText, containsString("http://28"))
-    }
-
-    @Test
-    fun outputContainsColumnHeadersForAllColumns() {
-        val currentState = WeatherState("Weather", ZonedDateTime.of(2016, 5, 28, 0, 0, 0, 0, of("Europe/Berlin")))
-        currentState += HourState(0, 10, 11, 0.12, 13.0, WindDirection.SOUTHSOUTHEAST, 14, 15, 0.16, "17", "http://18")
-        currentState += HourState(1, 20, 21, 0.22, 23.0, WindDirection.NORTHNORTHWEST, 24, 25, 0.26, "27", "http://28")
-        trigger.mergeStates(previousState, currentState)
-        val reaction = mock(Reaction::class.java)
-        val output = trigger.output(reaction)
-        val htmlText = output.text("text/html")
-        assertThat(htmlText, containsString("Time"))
-        assertThat(htmlText, containsString("Temperature"))
-        assertThat(htmlText, containsString("feels like"))
-        assertThat(htmlText, containsString("Chance of Rain"))
-        assertThat(htmlText, containsString("Amount"))
-        assertThat(htmlText, containsString("Wind from"))
-        assertThat(htmlText, containsString("Speed"))
-        assertThat(htmlText, containsString("Gusts"))
-        assertThat(htmlText, containsString("Humidity"))
-        assertThat(htmlText, containsString("Description"))
-        assertThat(htmlText, containsString("Image"))
-    }
-
-    @Test
-    fun outputDoesNotContainColumnHeadersForMissingColumns() {
-        val currentState = WeatherState("Weather", ZonedDateTime.of(2016, 5, 28, 0, 0, 0, 0, of("Europe/Berlin")))
-        currentState += HourState(0, 10, null, 0.12, 13.0, WindDirection.SOUTHSOUTHEAST, 14, null, null, "17", "http://18")
-        currentState += HourState(1, 20, null, 0.22, 23.0, WindDirection.NORTHNORTHWEST, 24, null, null, "27", "http://28")
-        trigger.mergeStates(previousState, currentState)
-        val reaction = mock(Reaction::class.java)
-        val output = trigger.output(reaction)
-        val htmlText = output.text("text/html")
-        assertThat(htmlText, containsString("Time"))
-        assertThat(htmlText, containsString("Temperature"))
-        assertThat(htmlText, not(containsString("feels like")))
-        assertThat(htmlText, containsString("Chance of Rain"))
-        assertThat(htmlText, containsString("Amount"))
-        assertThat(htmlText, containsString("Wind from"))
-        assertThat(htmlText, containsString("Speed"))
-        assertThat(htmlText, not(containsString("Gusts")))
-        assertThat(htmlText, not(containsString("Humidity")))
-        assertThat(htmlText, containsString("Description"))
-        assertThat(htmlText, containsString("Image"))
-    }
-
 }
index f251cf8..a2430f1 100644 (file)
@@ -1,9 +1,9 @@
 package net.pterodactylus.rhynodge.webpages.weather.wetterde
 
 import net.pterodactylus.rhynodge.filters.ResourceLoader
-import net.pterodactylus.rhynodge.states.AbstractState
 import net.pterodactylus.rhynodge.states.FailedState
 import net.pterodactylus.rhynodge.states.HtmlState
+import net.pterodactylus.rhynodge.states.StateManagerTest.TestState
 import net.pterodactylus.rhynodge.webpages.weather.HourState
 import net.pterodactylus.rhynodge.webpages.weather.WeatherState
 import net.pterodactylus.rhynodge.webpages.weather.WindDirection
@@ -36,7 +36,7 @@ class WetterDeFilterTest {
 
     @Test
     fun filterThrowsExceptionIfNotGivenAnHtmlState() {
-        val successfulHonHtmlState = object : AbstractState(true) {}
+        val successfulHonHtmlState = TestState()
         expectedException.expect(IllegalArgumentException::class.java)
         filter.filter(successfulHonHtmlState)
     }