From 9c01d55d3969a1b3df6529df0c64d4feb146fe4d Mon Sep 17 00:00:00 2001 From: =?utf8?q?David=20=E2=80=98Bombe=E2=80=99=20Roden?= Date: Mon, 18 Feb 2013 21:58:45 +0100 Subject: [PATCH] Enhance trigger interface to allow merging states. --- .../java/net/pterodactylus/rhynodge/Trigger.java | 23 +++-- .../net/pterodactylus/rhynodge/engine/Engine.java | 16 ++- .../rhynodge/triggers/AlwaysTrigger.java | 14 ++- .../rhynodge/triggers/FileExistenceTrigger.java | 16 ++- .../triggers/FileStateModifiedTrigger.java | 16 ++- .../rhynodge/triggers/NewEpisodeTrigger.java | 115 ++++++++++++--------- .../rhynodge/triggers/NewTorrentTrigger.java | 40 ++++--- 7 files changed, 165 insertions(+), 75 deletions(-) diff --git a/src/main/java/net/pterodactylus/rhynodge/Trigger.java b/src/main/java/net/pterodactylus/rhynodge/Trigger.java index 3b5a730..e4024f2 100644 --- a/src/main/java/net/pterodactylus/rhynodge/Trigger.java +++ b/src/main/java/net/pterodactylus/rhynodge/Trigger.java @@ -31,20 +31,31 @@ import net.pterodactylus.rhynodge.states.FileState; public interface Trigger { /** - * Checks whether the given states warrant a change trigger. + * Merges the current state into the previous state, returning the merged + * state. * - * @param currentState - * The current state of a system * @param previousState * The previous state of the system - * @return {@code true} if the given states warrant a change trigger, + * @param currentState + * The current state of a system + * @return The new state, containing a meaningful merge between the previous + * and the current state + */ + State mergeStates(State previousState, State currentState); + + /** + * Checks whether the states given to {@link #mergeStates(State, State)} + * warrant a change trigger. + * + * @return {@code true} if the states given to + * {@link #mergeStates(State, State)} warrant a change trigger, * {@code false} otherwise */ - boolean triggers(State currentState, State previousState); + boolean triggers(); /** * Returns the output of this trigger. This will only return a meaningful - * value if {@link #triggers(State, State)} returns {@code true}. + * value if {@link #triggers()} returns {@code true}. * * @param reaction * The reaction being triggered diff --git a/src/main/java/net/pterodactylus/rhynodge/engine/Engine.java b/src/main/java/net/pterodactylus/rhynodge/engine/Engine.java index 9e4d883..23fa69b 100644 --- a/src/main/java/net/pterodactylus/rhynodge/engine/Engine.java +++ b/src/main/java/net/pterodactylus/rhynodge/engine/Engine.java @@ -188,14 +188,20 @@ public class Engine extends AbstractExecutionThreadService { state.setFailCount(lastStateFailCount + 1); } net.pterodactylus.rhynodge.State lastSuccessfulState = stateManager.loadLastSuccessfulState(reactionName); - stateManager.saveState(reactionName, state); - /* only run trigger if we have collected two successful states. */ - Trigger trigger = nextReaction.trigger(); + /* merge states. */ boolean triggerHit = false; + Trigger trigger = nextReaction.trigger(); if ((lastSuccessfulState != null) && lastSuccessfulState.success() && state.success()) { - logger.debug("Checking Trigger for changes..."); - triggerHit = trigger.triggers(state, lastSuccessfulState); + net.pterodactylus.rhynodge.State newState = trigger.mergeStates(lastSuccessfulState, state); + + /* save new state. */ + stateManager.saveState(reactionName, newState); + + triggerHit = trigger.triggers(); + } else { + /* save first or error state. */ + stateManager.saveState(reactionName, state); } /* run action if trigger was hit. */ diff --git a/src/main/java/net/pterodactylus/rhynodge/triggers/AlwaysTrigger.java b/src/main/java/net/pterodactylus/rhynodge/triggers/AlwaysTrigger.java index 3716894..5def993 100644 --- a/src/main/java/net/pterodactylus/rhynodge/triggers/AlwaysTrigger.java +++ b/src/main/java/net/pterodactylus/rhynodge/triggers/AlwaysTrigger.java @@ -32,9 +32,21 @@ public class AlwaysTrigger implements Trigger { /** * {@inheritDoc} + *

+ * This implementation returns the current state. */ @Override - public boolean triggers(State currentState, State previousState) { + public State mergeStates(State previousState, State currentState) { + return currentState; + } + + /** + * {@inheritDoc} + *

+ * This implementation always returns {@code true}. + */ + @Override + public boolean triggers() { return true; } diff --git a/src/main/java/net/pterodactylus/rhynodge/triggers/FileExistenceTrigger.java b/src/main/java/net/pterodactylus/rhynodge/triggers/FileExistenceTrigger.java index 49d91b2..264740f 100644 --- a/src/main/java/net/pterodactylus/rhynodge/triggers/FileExistenceTrigger.java +++ b/src/main/java/net/pterodactylus/rhynodge/triggers/FileExistenceTrigger.java @@ -33,6 +33,9 @@ import com.google.common.base.Preconditions; */ public class FileExistenceTrigger implements Trigger { + /** Whether a change is triggered. */ + private boolean triggered; + // // TRIGGER METHODS // @@ -41,10 +44,19 @@ public class FileExistenceTrigger implements Trigger { * {@inheritDoc} */ @Override - public boolean triggers(State previousState, State currentState) { + public State mergeStates(State previousState, State currentState) { Preconditions.checkState(previousState instanceof FileState, "previousState is not a FileState"); Preconditions.checkState(currentState instanceof FileState, "currentState is not a FileState"); - return ((FileState) previousState).exists() != ((FileState) currentState).exists(); + triggered = ((FileState) previousState).exists() != ((FileState) currentState).exists(); + return currentState; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean triggers() { + return triggered; } /** diff --git a/src/main/java/net/pterodactylus/rhynodge/triggers/FileStateModifiedTrigger.java b/src/main/java/net/pterodactylus/rhynodge/triggers/FileStateModifiedTrigger.java index ed24d9e..7b3026c 100644 --- a/src/main/java/net/pterodactylus/rhynodge/triggers/FileStateModifiedTrigger.java +++ b/src/main/java/net/pterodactylus/rhynodge/triggers/FileStateModifiedTrigger.java @@ -34,16 +34,28 @@ import net.pterodactylus.rhynodge.states.FileState; */ public class FileStateModifiedTrigger implements Trigger { + /** Whether a change was triggered. */ + private boolean triggered; + /** * {@inheritDoc} */ @Override - public boolean triggers(State currentState, State previousState) { + public State mergeStates(State previousState, State currentState) { checkState(currentState instanceof FileState, "currentState is not a FileState but a %s", currentState.getClass()); checkState(previousState instanceof FileState, "previousState is not a FileState but a %s", currentState.getClass()); FileState currentFileState = (FileState) currentState; FileState previousFileState = (FileState) previousState; - return (currentFileState.exists() != previousFileState.exists()) || (currentFileState.size() != previousFileState.size()) || (currentFileState.modificationTime() != previousFileState.modificationTime()); + triggered = (currentFileState.exists() != previousFileState.exists()) || (currentFileState.size() != previousFileState.size()) || (currentFileState.modificationTime() != previousFileState.modificationTime()); + return currentState; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean triggers() { + return triggered; } /** diff --git a/src/main/java/net/pterodactylus/rhynodge/triggers/NewEpisodeTrigger.java b/src/main/java/net/pterodactylus/rhynodge/triggers/NewEpisodeTrigger.java index a8e321c..421f6e1 100644 --- a/src/main/java/net/pterodactylus/rhynodge/triggers/NewEpisodeTrigger.java +++ b/src/main/java/net/pterodactylus/rhynodge/triggers/NewEpisodeTrigger.java @@ -20,6 +20,8 @@ package net.pterodactylus.rhynodge.triggers; import static com.google.common.base.Preconditions.checkState; import java.util.Collection; +import java.util.Map; +import java.util.Map.Entry; import net.pterodactylus.rhynodge.Reaction; import net.pterodactylus.rhynodge.State; @@ -32,8 +34,11 @@ import net.pterodactylus.rhynodge.states.TorrentState.TorrentFile; import org.apache.commons.lang3.StringEscapeUtils; -import com.google.common.base.Predicate; -import com.google.common.collect.Collections2; +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 com.google.common.collect.Sets; /** * {@link Trigger} implementation that compares two {@link EpisodeState}s for @@ -44,67 +49,58 @@ import com.google.common.collect.Collections2; public class NewEpisodeTrigger implements Trigger { /** All new episodes. */ - private Collection newEpisodes; + private final Collection newEpisodes = Sets.newHashSet(); /** All changed episodes. */ - private Collection changedEpisodes; + private final Collection changedEpisodes = Sets.newHashSet(); + + /** All episodes. */ + private final Collection allEpisodes = Sets.newHashSet(); // // TRIGGER METHODS // /** - * {@inheritDoc} + * {@inheritDocs} */ @Override - public boolean triggers(State currentState, State previousState) { + 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()); - final EpisodeState currentEpisodeState = (EpisodeState) currentState; - final EpisodeState previousEpisodeState = (EpisodeState) previousState; - - newEpisodes = Collections2.filter(currentEpisodeState.episodes(), new Predicate() { + newEpisodes.clear(); + changedEpisodes.clear(); + this.allEpisodes.clear(); + Map allEpisodes = FluentIterable.from(((EpisodeState) previousState).episodes()).toMap(new Function() { @Override - public boolean apply(Episode episode) { - return !previousEpisodeState.episodes().contains(episode); + public Episode apply(Episode episode) { + return episode; } }); - - changedEpisodes = Collections2.filter(currentEpisodeState.episodes(), new Predicate() { - - @Override - public boolean apply(Episode episode) { - if (!previousEpisodeState.episodes().contains(episode)) { - return false; - } - - /* find previous episode. */ - final Episode previousEpisode = findPreviousEpisode(episode); - - /* compare the list of torrent files. */ - Collection newTorrentFiles = Collections2.filter(episode.torrentFiles(), new Predicate() { - - @Override - public boolean apply(TorrentFile torrentFile) { - return !previousEpisode.torrentFiles().contains(torrentFile); - } - }); - - return !newTorrentFiles.isEmpty(); + for (Episode episode : ((EpisodeState) currentState).episodes()) { + if (!allEpisodes.containsKey(episode)) { + allEpisodes.put(episode, episode); + newEpisodes.add(episode); } - - private Episode findPreviousEpisode(Episode episode) { - for (Episode previousStateEpisode : previousEpisodeState) { - if (previousStateEpisode.equals(episode)) { - return previousStateEpisode; - } + for (TorrentFile torrentFile : episode.torrentFiles()) { + int oldSize = allEpisodes.get(episode).torrentFiles().size(); + allEpisodes.get(episode).addTorrentFile(torrentFile); + int newSize = allEpisodes.get(episode).torrentFiles().size(); + if (!newEpisodes.contains(episode) && (oldSize != newSize)) { + changedEpisodes.add(episode); } - return null; } + } + this.allEpisodes.addAll(allEpisodes.values()); + return new EpisodeState(this.allEpisodes); + } - }); - + /** + * {@inheritDoc} + */ + @Override + public boolean triggers() { return !newEpisodes.isEmpty() || !changedEpisodes.isEmpty(); } @@ -124,8 +120,8 @@ public class NewEpisodeTrigger implements Trigger { summary = String.format("%d changed Torrent(s) for “%s!”", changedEpisodes.size(), reaction.name()); } DefaultOutput output = new DefaultOutput(summary); - output.addText("text/plain", generatePlainText(reaction, newEpisodes, changedEpisodes)); - output.addText("text/html", generateHtmlText(reaction, newEpisodes, changedEpisodes)); + output.addText("text/plain", generatePlainText(reaction, newEpisodes, changedEpisodes, allEpisodes)); + output.addText("text/html", generateHtmlText(reaction, newEpisodes, changedEpisodes, allEpisodes)); return output; } @@ -142,9 +138,11 @@ public class NewEpisodeTrigger implements Trigger { * The new episodes * @param changedEpisodes * The changed episodes + * @param allEpisodes + * All episodes * @return The plain text output */ - private static String generatePlainText(Reaction reaction, Collection newEpisodes, Collection changedEpisodes) { + private static String generatePlainText(Reaction reaction, Collection newEpisodes, Collection changedEpisodes, Collection allEpisodes) { StringBuilder stringBuilder = new StringBuilder(); if (!newEpisodes.isEmpty()) { stringBuilder.append(reaction.name()).append(" - New Episodes\n\n"); @@ -176,6 +174,27 @@ public class NewEpisodeTrigger implements Trigger { } } } + /* list all known episodes. */ + stringBuilder.append(reaction.name()).append(" - All Known Episodes\n\n"); + ImmutableMap> episodesBySeason = FluentIterable.from(allEpisodes).index(new Function() { + + @Override + public Integer apply(Episode episode) { + return episode.season(); + } + }).asMap(); + for (Entry> 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(); } @@ -188,9 +207,11 @@ public class NewEpisodeTrigger implements Trigger { * The new episodes * @param changedEpisodes * The changed episodes + * @param allEpisodes + * All episodes * @return The HTML output */ - private static String generateHtmlText(Reaction reaction, Collection newEpisodes, Collection changedEpisodes) { + private static String generateHtmlText(Reaction reaction, Collection newEpisodes, Collection changedEpisodes, Collection allEpisodes) { StringBuilder htmlBuilder = new StringBuilder(); htmlBuilder.append("\n"); htmlBuilder.append("

").append(StringEscapeUtils.escapeHtml4(reaction.name())).append("

\n"); diff --git a/src/main/java/net/pterodactylus/rhynodge/triggers/NewTorrentTrigger.java b/src/main/java/net/pterodactylus/rhynodge/triggers/NewTorrentTrigger.java index 2803bb5..12c7f8c 100644 --- a/src/main/java/net/pterodactylus/rhynodge/triggers/NewTorrentTrigger.java +++ b/src/main/java/net/pterodactylus/rhynodge/triggers/NewTorrentTrigger.java @@ -20,6 +20,7 @@ package net.pterodactylus.rhynodge.triggers; import static com.google.common.base.Preconditions.checkState; import java.util.List; +import java.util.Set; import net.pterodactylus.rhynodge.Reaction; import net.pterodactylus.rhynodge.State; @@ -32,6 +33,7 @@ import net.pterodactylus.rhynodge.states.TorrentState.TorrentFile; import org.apache.commons.lang3.StringEscapeUtils; import com.google.common.collect.Lists; +import com.google.common.collect.Sets; /** * {@link Trigger} implementation that is triggered by {@link TorrentFile}s that @@ -41,30 +43,44 @@ import com.google.common.collect.Lists; */ public class NewTorrentTrigger implements Trigger { + /** All known torrents. */ + private final Set allTorrentFiles = Sets.newHashSet(); + /** The newly detected torrent files. */ - private List torrentFiles = Lists.newArrayList(); + private final List newTorrentFiles = Lists.newArrayList(); // // TRIGGER METHODS // /** - * {@inheritDoc} + * {@inheritDocs} */ @Override - public boolean triggers(State currentState, State previousState) { + 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; - torrentFiles.clear(); + + allTorrentFiles.clear(); + newTorrentFiles.clear(); + allTorrentFiles.addAll(previousTorrentState.torrentFiles()); for (TorrentFile torrentFile : currentTorrentState) { - torrentFiles.add(torrentFile); - } - for (TorrentFile torrentFile : previousTorrentState) { - torrentFiles.remove(torrentFile); + if (allTorrentFiles.add(torrentFile)) { + newTorrentFiles.add(torrentFile); + } } - return !torrentFiles.isEmpty(); + + return new TorrentState(allTorrentFiles); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean triggers() { + return !newTorrentFiles.isEmpty(); } /** @@ -72,9 +88,9 @@ public class NewTorrentTrigger implements Trigger { */ @Override public Output output(Reaction reaction) { - DefaultOutput output = new DefaultOutput(String.format("Found %d new Torrent(s) for “%s!”", torrentFiles.size(), reaction.name())); - output.addText("text/plain", getPlainTextList(torrentFiles)); - output.addText("text/html", getHtmlTextList(torrentFiles)); + DefaultOutput output = new DefaultOutput(String.format("Found %d new Torrent(s) for “%s!”", newTorrentFiles.size(), reaction.name())); + output.addText("text/plain", getPlainTextList(newTorrentFiles)); + output.addText("text/html", getHtmlTextList(newTorrentFiles)); return output; } -- 2.7.4