Rename project to “Rhynodge.”
authorDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Thu, 10 Jan 2013 18:04:51 +0000 (19:04 +0100)
committerDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Thu, 10 Jan 2013 18:04:51 +0000 (19:04 +0100)
74 files changed:
README.md
pom.xml
src/main/java/net/pterodactylus/reactor/Action.java [deleted file]
src/main/java/net/pterodactylus/reactor/Filter.java [deleted file]
src/main/java/net/pterodactylus/reactor/Query.java [deleted file]
src/main/java/net/pterodactylus/reactor/Reaction.java [deleted file]
src/main/java/net/pterodactylus/reactor/State.java [deleted file]
src/main/java/net/pterodactylus/reactor/Trigger.java [deleted file]
src/main/java/net/pterodactylus/reactor/actions/EmailAction.java [deleted file]
src/main/java/net/pterodactylus/reactor/actions/StandardOutAction.java [deleted file]
src/main/java/net/pterodactylus/reactor/engine/Engine.java [deleted file]
src/main/java/net/pterodactylus/reactor/engine/Starter.java [deleted file]
src/main/java/net/pterodactylus/reactor/filters/EpisodeFilter.java [deleted file]
src/main/java/net/pterodactylus/reactor/filters/HtmlFilter.java [deleted file]
src/main/java/net/pterodactylus/reactor/filters/KickAssTorrentsFilter.java [deleted file]
src/main/java/net/pterodactylus/reactor/loader/Chain.java [deleted file]
src/main/java/net/pterodactylus/reactor/loader/ChainWatcher.java [deleted file]
src/main/java/net/pterodactylus/reactor/loader/LoaderException.java [deleted file]
src/main/java/net/pterodactylus/reactor/loader/ReactionLoader.java [deleted file]
src/main/java/net/pterodactylus/reactor/output/DefaultOutput.java [deleted file]
src/main/java/net/pterodactylus/reactor/output/Output.java [deleted file]
src/main/java/net/pterodactylus/reactor/package-info.java [deleted file]
src/main/java/net/pterodactylus/reactor/queries/FileQuery.java [deleted file]
src/main/java/net/pterodactylus/reactor/queries/HttpQuery.java [deleted file]
src/main/java/net/pterodactylus/reactor/states/AbstractState.java [deleted file]
src/main/java/net/pterodactylus/reactor/states/EpisodeState.java [deleted file]
src/main/java/net/pterodactylus/reactor/states/FailedState.java [deleted file]
src/main/java/net/pterodactylus/reactor/states/FileState.java [deleted file]
src/main/java/net/pterodactylus/reactor/states/HtmlState.java [deleted file]
src/main/java/net/pterodactylus/reactor/states/HttpState.java [deleted file]
src/main/java/net/pterodactylus/reactor/states/StateManager.java [deleted file]
src/main/java/net/pterodactylus/reactor/states/TorrentState.java [deleted file]
src/main/java/net/pterodactylus/reactor/triggers/AlwaysTrigger.java [deleted file]
src/main/java/net/pterodactylus/reactor/triggers/FileExistenceTrigger.java [deleted file]
src/main/java/net/pterodactylus/reactor/triggers/FileStateModifiedTrigger.java [deleted file]
src/main/java/net/pterodactylus/reactor/triggers/NewEpisodeTrigger.java [deleted file]
src/main/java/net/pterodactylus/reactor/triggers/NewTorrentTrigger.java [deleted file]
src/main/java/net/pterodactylus/rhynodge/Action.java [new file with mode: 0644]
src/main/java/net/pterodactylus/rhynodge/Filter.java [new file with mode: 0644]
src/main/java/net/pterodactylus/rhynodge/Query.java [new file with mode: 0644]
src/main/java/net/pterodactylus/rhynodge/Reaction.java [new file with mode: 0644]
src/main/java/net/pterodactylus/rhynodge/State.java [new file with mode: 0644]
src/main/java/net/pterodactylus/rhynodge/Trigger.java [new file with mode: 0644]
src/main/java/net/pterodactylus/rhynodge/actions/EmailAction.java [new file with mode: 0644]
src/main/java/net/pterodactylus/rhynodge/actions/StandardOutAction.java [new file with mode: 0644]
src/main/java/net/pterodactylus/rhynodge/engine/Engine.java [new file with mode: 0644]
src/main/java/net/pterodactylus/rhynodge/engine/Starter.java [new file with mode: 0644]
src/main/java/net/pterodactylus/rhynodge/filters/EpisodeFilter.java [new file with mode: 0644]
src/main/java/net/pterodactylus/rhynodge/filters/HtmlFilter.java [new file with mode: 0644]
src/main/java/net/pterodactylus/rhynodge/filters/KickAssTorrentsFilter.java [new file with mode: 0644]
src/main/java/net/pterodactylus/rhynodge/loader/Chain.java [new file with mode: 0644]
src/main/java/net/pterodactylus/rhynodge/loader/ChainWatcher.java [new file with mode: 0644]
src/main/java/net/pterodactylus/rhynodge/loader/LoaderException.java [new file with mode: 0644]
src/main/java/net/pterodactylus/rhynodge/loader/ReactionLoader.java [new file with mode: 0644]
src/main/java/net/pterodactylus/rhynodge/output/DefaultOutput.java [new file with mode: 0644]
src/main/java/net/pterodactylus/rhynodge/output/Output.java [new file with mode: 0644]
src/main/java/net/pterodactylus/rhynodge/package-info.java [new file with mode: 0644]
src/main/java/net/pterodactylus/rhynodge/queries/FileQuery.java [new file with mode: 0644]
src/main/java/net/pterodactylus/rhynodge/queries/HttpQuery.java [new file with mode: 0644]
src/main/java/net/pterodactylus/rhynodge/states/AbstractState.java [new file with mode: 0644]
src/main/java/net/pterodactylus/rhynodge/states/EpisodeState.java [new file with mode: 0644]
src/main/java/net/pterodactylus/rhynodge/states/FailedState.java [new file with mode: 0644]
src/main/java/net/pterodactylus/rhynodge/states/FileState.java [new file with mode: 0644]
src/main/java/net/pterodactylus/rhynodge/states/HtmlState.java [new file with mode: 0644]
src/main/java/net/pterodactylus/rhynodge/states/HttpState.java [new file with mode: 0644]
src/main/java/net/pterodactylus/rhynodge/states/StateManager.java [new file with mode: 0644]
src/main/java/net/pterodactylus/rhynodge/states/TorrentState.java [new file with mode: 0644]
src/main/java/net/pterodactylus/rhynodge/triggers/AlwaysTrigger.java [new file with mode: 0644]
src/main/java/net/pterodactylus/rhynodge/triggers/FileExistenceTrigger.java [new file with mode: 0644]
src/main/java/net/pterodactylus/rhynodge/triggers/FileStateModifiedTrigger.java [new file with mode: 0644]
src/main/java/net/pterodactylus/rhynodge/triggers/NewEpisodeTrigger.java [new file with mode: 0644]
src/main/java/net/pterodactylus/rhynodge/triggers/NewTorrentTrigger.java [new file with mode: 0644]
src/main/resources/chains/kickasstorrents-example.json
src/main/resources/log4j.properties

index a17c584..1a01145 100644 (file)
--- a/README.md
+++ b/README.md
@@ -1,14 +1,14 @@
-# Reactor
+# Rhynodge
 
 ## Description
 
-Reactor is a tool that lets you periodically execute tasks that depend on certain conditions.
+Rhynodge is a tool that lets you periodically execute tasks that depend on certain conditions.
 
 Its concept is quite similar to websites like ifttt (“if this then that”): you evaluate an input condition (e. g. data from a website, Facebook or Twitter posts, incoming emails, existence of a file), and if it evaluates to “yes” you execute a certain action.
 
 ## Concepts
 
-The core of Reactor comprises ``Reaction``s which in turn consist of ``Query``s, ``Filter``s, ``Trigger``s, and ``Action``s.
+The core of Rhynodge comprises ``Reaction``s which in turn consist of ``Query``s, ``Filter``s, ``Trigger``s, and ``Action``s.
 
 ### Query
 
@@ -34,4 +34,4 @@ If a trigger found a change, the action is then executed. Again, an action can b
 
 ## Configuration
 
-Reactor’s configuration uses JSON files (I tried using XML first but apparently polymorphic deserialization is something that is not easily done with XML parsers). The format of a ``Chain`` configuration is pretty straight-forward and can be seen in the example configuration files.
+Rhynodge’s configuration uses JSON files (I tried using XML first but apparently polymorphic deserialization is something that is not easily done with XML parsers). The format of a ``Chain`` configuration is pretty straight-forward and can be seen in the example configuration files.
diff --git a/pom.xml b/pom.xml
index 0130d8b..df78686 100644 (file)
--- a/pom.xml
+++ b/pom.xml
@@ -2,7 +2,7 @@
        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
        <groupId>net.pterodactylus</groupId>
-       <artifactId>reactor</artifactId>
+       <artifactId>rhynodge</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <properties>
                <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
@@ -22,7 +22,7 @@
                                <artifactId>exec-maven-plugin</artifactId>
                                <version>1.2.1</version>
                                <configuration>
-                                       <mainClass>net.pterodactylus.reactor.engine.Starter</mainClass>
+                                       <mainClass>net.pterodactylus.rhynodge.engine.Starter</mainClass>
                                </configuration>
                        </plugin>
                </plugins>
diff --git a/src/main/java/net/pterodactylus/reactor/Action.java b/src/main/java/net/pterodactylus/reactor/Action.java
deleted file mode 100644 (file)
index 650c3ad..0000000
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * Reactor - Action.java - Copyright © 2013 David Roden
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.reactor;
-
-import net.pterodactylus.reactor.output.Output;
-
-/**
- * An action is performed when a {@link Trigger} determines that two given
- * {@link State}s of a {@link Query} signify a change.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public interface Action {
-
-       /**
-        * Performs the action.
-        *
-        * @param output
-        *            The output for the action
-        */
-       void execute(Output output);
-
-}
diff --git a/src/main/java/net/pterodactylus/reactor/Filter.java b/src/main/java/net/pterodactylus/reactor/Filter.java
deleted file mode 100644 (file)
index 0afb226..0000000
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- * Reactor - Filter.java - Copyright © 2013 David Roden
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.reactor;
-
-/**
- * Defines a filter that runs between {@link Query}s and {@link Trigger}s and
- * can be used to convert a {@link State} into another {@link State}. This can
- * be used to extract further information from a state.
- * <p>
- * An example scenario would be a {@link Query} that requests a web site and a
- * {@link Filter} that extracts content from the web site. That way the same
- * {@link Query} could be used for multiple {@link Reaction}s without requiring
- * modifications.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public interface Filter {
-
-       /**
-        * Converts the given state into a different state.
-        *
-        * @param state
-        *            The state to convert
-        * @return The new state
-        */
-       State filter(State state);
-
-}
diff --git a/src/main/java/net/pterodactylus/reactor/Query.java b/src/main/java/net/pterodactylus/reactor/Query.java
deleted file mode 100644 (file)
index 0b46a39..0000000
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- * Reactor - Query.java - Copyright © 2013 David Roden
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.reactor;
-
-/**
- * A query is used to retrieve the current {@link State} of a system.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public interface Query {
-
-       /**
-        * Retrieves the current state of the system. The returned state is never
-        * {@code null}.
-        *
-        * @return The current state of the system.
-        */
-       public State state();
-
-}
diff --git a/src/main/java/net/pterodactylus/reactor/Reaction.java b/src/main/java/net/pterodactylus/reactor/Reaction.java
deleted file mode 100644 (file)
index 65a637b..0000000
+++ /dev/null
@@ -1,160 +0,0 @@
-/*
- * Reactor - Reaction.java - Copyright © 2013 David Roden
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.reactor;
-
-import java.util.Collections;
-import java.util.List;
-
-import com.google.common.collect.Lists;
-
-/**
- * A {@code Reaction} binds together {@link Query}s, {@link Trigger}s, and
- * {@link Action}s, and it stores the intermediary {@link State}s.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class Reaction {
-
-       /** The name of this reaction. */
-       private final String name;
-
-       /** The query to run. */
-       private final Query query;
-
-       /** The filters to run. */
-       private final List<Filter> filters = Lists.newArrayList();
-
-       /** The trigger to detect changes. */
-       private final Trigger trigger;
-
-       /** The action to perform. */
-       private final Action action;
-
-       /** The interval in which to run queries (in milliseconds). */
-       private long updateInterval;
-
-       /**
-        * Creates a new reaction.
-        *
-        * @param name
-        *            The name of the reaction
-        * @param query
-        *            The query to run
-        * @param trigger
-        *            The trigger to detect changes
-        * @param action
-        *            The action to perform
-        */
-       public Reaction(String name, Query query, Trigger trigger, Action action) {
-               this(name, query, Collections.<Filter> emptyList(), trigger, action);
-       }
-
-       /**
-        * Creates a new reaction.
-        *
-        * @param name
-        *            The name of the reaction
-        * @param query
-        *            The query to run
-        * @param filters
-        *            The filters to run
-        * @param trigger
-        *            The trigger to detect changes
-        * @param action
-        *            The action to perform
-        */
-       public Reaction(String name, Query query, List<Filter> filters, Trigger trigger, Action action) {
-               this.name = name;
-               this.query = query;
-               this.filters.addAll(filters);
-               this.trigger = trigger;
-               this.action = action;
-       }
-
-       //
-       // ACCESSORS
-       //
-
-       /**
-        * Returns the name of this reaction. This name is solely used for display
-        * purposes and does not need to be unique.
-        *
-        * @return The name of this reaction
-        */
-       public String name() {
-               return name;
-       }
-
-       /**
-        * Returns the query to run.
-        *
-        * @return The query to run
-        */
-       public Query query() {
-               return query;
-       }
-
-       /**
-        * Returns the filters to run.
-        *
-        * @return The filters to run
-        */
-       public Iterable<Filter> filters() {
-               return filters;
-       }
-
-       /**
-        * Returns the trigger to detect changes.
-        *
-        * @return The trigger to detect changes
-        */
-       public Trigger trigger() {
-               return trigger;
-       }
-
-       /**
-        * Returns the action to perform.
-        *
-        * @return The action to perform
-        */
-       public Action action() {
-               return action;
-       }
-
-       /**
-        * Returns the update interval of this reaction.
-        *
-        * @return The update interval of this reaction (in milliseconds)
-        */
-       public long updateInterval() {
-               return updateInterval;
-       }
-
-       /**
-        * Sets the update interval of this reaction.
-        *
-        * @param updateInterval
-        *            The update interval of this reaction (in milliseconds)
-        * @return This reaction
-        */
-       public Reaction setUpdateInterval(long updateInterval) {
-               this.updateInterval = updateInterval;
-               return this;
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/reactor/State.java b/src/main/java/net/pterodactylus/reactor/State.java
deleted file mode 100644 (file)
index 278032e..0000000
+++ /dev/null
@@ -1,77 +0,0 @@
-/*
- * Reactor - State.java - Copyright © 2013 David Roden
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.reactor;
-
-/**
- * Defines the current state of a system.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public interface State {
-
-       /**
-        * Returns the time when this state was retrieved.
-        *
-        * @return The time when this state was retrieved (in millseconds since Jan
-        *         1, 1970 UTC)
-        */
-       long time();
-
-       /**
-        * Whether the state was successfully retrieved. This method should only
-        * return {@code true} if a meaningful result could be retrieved; if e. g. a
-        * service is currently not reachable, this method should return false
-        * instead of emulating success by using empty lists or similar constructs.
-        *
-        * @return {@code true} if the state could be retrieved successfully,
-        *         {@code false} otherwise
-        */
-       boolean success();
-
-       /**
-        * Returns the number of consecutive failures. This method only returns a
-        * meaningful number iff {@link #success()} returns {@code false}. If
-        * {@link #success()} returns {@code false} for the first time after
-        * returning {@code true} and this method is called after {@link #success()}
-        * it will return {@code 1}.
-        *
-        * @return The number of consecutive failures
-        */
-       int failCount();
-
-       /**
-        * Sets the fail count of this state.
-        *
-        * @param failCount
-        *            The fail count of this state
-        */
-       void setFailCount(int failCount);
-
-       /**
-        * If {@link #success()} returns {@code false}, this method may return a
-        * {@link Throwable} to give some details for the reason why retrieving the
-        * state was not possible. For example, network-based {@link Query}s might
-        * return any exception that were encountered while communicating with the
-        * remote service.
-        *
-        * @return An exception that occured, may be {@code null} in case an
-        *         exception can not be meaningfully returned
-        */
-       Throwable exception();
-
-}
diff --git a/src/main/java/net/pterodactylus/reactor/Trigger.java b/src/main/java/net/pterodactylus/reactor/Trigger.java
deleted file mode 100644 (file)
index 277eeb6..0000000
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * Reactor - Trigger.java - Copyright © 2013 David Roden
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.reactor;
-
-import net.pterodactylus.reactor.output.Output;
-import net.pterodactylus.reactor.states.FileState;
-
-/**
- * A trigger determines whether two different states actually warrant a change
- * trigger. For example, two {@link FileState}s might contain different file
- * sizes but a trigger might only care about whether the file appeared or
- * disappeared since the last check.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public interface Trigger {
-
-       /**
-        * Checks whether the given states warrant a change trigger.
-        *
-        * @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,
-        *         {@code false} otherwise
-        */
-       boolean triggers(State currentState, State previousState);
-
-       /**
-        * Returns the output of this trigger. This will only return a meaningful
-        * value if {@link #triggers(State, State)} returns {@code true}.
-        *
-        * @param reaction
-        *            The reaction being triggered
-        * @return The output of this trigger
-        */
-       Output output(Reaction reaction);
-
-}
diff --git a/src/main/java/net/pterodactylus/reactor/actions/EmailAction.java b/src/main/java/net/pterodactylus/reactor/actions/EmailAction.java
deleted file mode 100644 (file)
index ef9d602..0000000
+++ /dev/null
@@ -1,103 +0,0 @@
-/*
- * Reactor - EmailAction.java - Copyright © 2013 David Roden
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.reactor.actions;
-
-import java.util.Properties;
-
-import javax.mail.Message.RecipientType;
-import javax.mail.MessagingException;
-import javax.mail.Session;
-import javax.mail.Transport;
-import javax.mail.internet.InternetAddress;
-import javax.mail.internet.MimeBodyPart;
-import javax.mail.internet.MimeMessage;
-import javax.mail.internet.MimeMultipart;
-
-import net.pterodactylus.reactor.Action;
-import net.pterodactylus.reactor.output.Output;
-
-/**
- * {@link Action} implementation that sends an email containing the triggering
- * object to an email address.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class EmailAction implements Action {
-
-       /** The name of the SMTP host. */
-       private final String hostname;
-
-       /** The email address of the sender. */
-       private final String sender;
-
-       /** The email address of the recipient. */
-       private final String recipient;
-
-       /**
-        * Creates a new email action.
-        *
-        * @param hostname
-        *            The hostname of the SMTP server
-        * @param sender
-        *            The email address of the sender
-        * @param recipient
-        *            The email address of the recipient
-        */
-       public EmailAction(String hostname, String sender, String recipient) {
-               this.hostname = hostname;
-               this.sender = sender;
-               this.recipient = recipient;
-       }
-
-       //
-       // ACTION METHODS
-       //
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       public void execute(Output output) {
-               Properties properties = System.getProperties();
-               properties.put("mail.smtp.host", hostname);
-               Session session = Session.getInstance(properties);
-               MimeMessage message = new MimeMessage(session);
-               try {
-                       /* create message. */
-                       message.setFrom(new InternetAddress(sender));
-                       message.setRecipient(RecipientType.TO, new InternetAddress(recipient));
-                       message.setSubject(output.summary());
-
-                       /* create text and html parts. */
-                       MimeMultipart multipart = new MimeMultipart();
-                       multipart.setSubType("alternative");
-                       MimeBodyPart textPart = new MimeBodyPart();
-                       textPart.setContent(output.text("text/plain", -1), "text/plain;charset=utf-8");
-                       MimeBodyPart htmlPart = new MimeBodyPart();
-                       htmlPart.setContent(output.text("text/html", -1), "text/html;charset=utf-8");
-                       multipart.addBodyPart(textPart);
-                       multipart.addBodyPart(htmlPart);
-                       message.setContent(multipart);
-
-                       Transport.send(message);
-               } catch (MessagingException me1) {
-                       /* swallow. */
-               }
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/reactor/actions/StandardOutAction.java b/src/main/java/net/pterodactylus/reactor/actions/StandardOutAction.java
deleted file mode 100644 (file)
index 8cad092..0000000
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
- * Reactor - StandardOutAction.java - Copyright © 2013 David Roden
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.reactor.actions;
-
-import net.pterodactylus.reactor.Action;
-import net.pterodactylus.reactor.State;
-import net.pterodactylus.reactor.output.Output;
-
-/**
- * {@link Action} that simply dumps all {@link State}s to standard output.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class StandardOutAction implements Action {
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       public void execute(Output output) {
-               System.out.println(String.format("Triggered by %s.", output.text("text/plain", -1)));
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/reactor/engine/Engine.java b/src/main/java/net/pterodactylus/reactor/engine/Engine.java
deleted file mode 100644 (file)
index 6f4376b..0000000
+++ /dev/null
@@ -1,211 +0,0 @@
-/*
- * Reactor - Engine.java - Copyright © 2013 David Roden
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.reactor.engine;
-
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Map.Entry;
-import java.util.SortedMap;
-
-import net.pterodactylus.reactor.Filter;
-import net.pterodactylus.reactor.Query;
-import net.pterodactylus.reactor.Reaction;
-import net.pterodactylus.reactor.Trigger;
-import net.pterodactylus.reactor.states.AbstractState;
-import net.pterodactylus.reactor.states.FailedState;
-import net.pterodactylus.reactor.states.StateManager;
-
-import org.apache.commons.lang3.tuple.Pair;
-import org.apache.log4j.Logger;
-
-import com.google.common.collect.Maps;
-import com.google.common.util.concurrent.AbstractExecutionThreadService;
-
-/**
- * Reactor main engine.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class Engine extends AbstractExecutionThreadService {
-
-       /** The logger. */
-       private static final Logger logger = Logger.getLogger(Engine.class);
-
-       /** The state manager. */
-       private final StateManager stateManager;
-
-       /** All defined reactions. */
-       /* synchronize on itself. */
-       private final Map<String, Reaction> reactions = new HashMap<String, Reaction>();
-
-       /**
-        * Creates a new engine.
-        *
-        * @param stateManager
-        *            The state manager
-        */
-       public Engine(StateManager stateManager) {
-               this.stateManager = stateManager;
-       }
-
-       //
-       // ACCESSORS
-       //
-
-       /**
-        * Adds the given reaction to this engine.
-        *
-        * @param name
-        *            The name of the reaction
-        * @param reaction
-        *            The reaction to add to this engine
-        */
-       public void addReaction(String name, Reaction reaction) {
-               synchronized (reactions) {
-                       reactions.put(name, reaction);
-                       reactions.notifyAll();
-               }
-       }
-
-       /**
-        * Removes the reaction with the given name.
-        *
-        * @param name
-        *            The name of the reaction to remove
-        */
-       public void removeReaction(String name) {
-               synchronized (reactions) {
-                       if (!reactions.containsKey(name)) {
-                               return;
-                       }
-                       reactions.remove(name);
-                       reactions.notifyAll();
-               }
-       }
-
-       //
-       // ABSTRACTSERVICE METHODS
-       //
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       public void run() {
-               while (isRunning()) {
-
-                       /* delay if we have no reactions. */
-                       synchronized (reactions) {
-                               if (reactions.isEmpty()) {
-                                       logger.debug("Sleeping while no Reactions available.");
-                                       try {
-                                               reactions.wait();
-                                       } catch (InterruptedException ie1) {
-                                               /* ignore, we’re looping anyway. */
-                                       }
-                                       continue;
-                               }
-                       }
-
-                       /* find next reaction. */
-                       SortedMap<Long, Pair<String, Reaction>> nextReactions = Maps.newTreeMap();
-                       String reactionName;
-                       Reaction nextReaction;
-                       synchronized (reactions) {
-                               for (Entry<String, Reaction> reactionEntry : reactions.entrySet()) {
-                                       net.pterodactylus.reactor.State state = stateManager.loadLastState(reactionEntry.getKey());
-                                       long stateTime = (state != null) ? state.time() : 0;
-                                       nextReactions.put(stateTime + reactionEntry.getValue().updateInterval(), Pair.of(reactionEntry.getKey(), reactionEntry.getValue()));
-                               }
-                               reactionName = nextReactions.get(nextReactions.firstKey()).getLeft();
-                               nextReaction = nextReactions.get(nextReactions.firstKey()).getRight();
-                       }
-                       logger.debug(String.format("Next Reaction: %s.", reactionName));
-
-                       /* wait until the next reaction has to run. */
-                       net.pterodactylus.reactor.State lastState = stateManager.loadLastState(reactionName);
-                       long lastStateTime = (lastState != null) ? lastState.time() : 0;
-                       int lastStateFailCount = (lastState != null) ? lastState.failCount() : 0;
-                       long waitTime = (lastStateTime + nextReaction.updateInterval()) - System.currentTimeMillis();
-                       logger.debug(String.format("Time to wait for next Reaction: %d millseconds.", waitTime));
-                       if (waitTime > 0) {
-                               synchronized (reactions) {
-                                       try {
-                                               logger.info(String.format("Waiting until %tc.", lastStateTime + nextReaction.updateInterval()));
-                                               reactions.wait(waitTime);
-                                       } catch (InterruptedException ie1) {
-                                               /* we’re looping! */
-                                       }
-                               }
-
-                               /* re-start loop to check for new reactions. */
-                               continue;
-                       }
-
-                       /* run reaction. */
-                       logger.info(String.format("Running Query for %s...", reactionName));
-                       Query query = nextReaction.query();
-                       net.pterodactylus.reactor.State state;
-                       try {
-                               logger.debug("Querying system...");
-                               state = query.state();
-                               if (state == null) {
-                                       state = FailedState.INSTANCE;
-                               }
-                               logger.debug("System queried.");
-                       } catch (Throwable t1) {
-                               logger.warn("Querying system failed!", t1);
-                               state = new AbstractState(t1) {
-                                       /* no further state. */
-                               };
-                       }
-                       logger.debug(String.format("State is %s.", state));
-
-                       /* convert states. */
-                       for (Filter filter : nextReaction.filters()) {
-                               if (state.success()) {
-                                       net.pterodactylus.reactor.State newState = filter.filter(state);
-                                       logger.debug(String.format("Old state is %s, new state is %s.", state, newState));
-                                       state = newState;
-                               }
-                       }
-                       if (!state.success()) {
-                               state.setFailCount(lastStateFailCount + 1);
-                       }
-                       net.pterodactylus.reactor.State lastSuccessfulState = stateManager.loadLastSuccessfulState(reactionName);
-                       stateManager.saveState(reactionName, state);
-
-                       /* only run trigger if we have collected two successful states. */
-                       Trigger trigger = nextReaction.trigger();
-                       boolean triggerHit = false;
-                       if ((lastSuccessfulState != null) && lastSuccessfulState.success() && state.success()) {
-                               logger.debug("Checking Trigger for changes...");
-                               triggerHit = trigger.triggers(state, lastSuccessfulState);
-                       }
-
-                       /* run action if trigger was hit. */
-                       logger.debug(String.format("Trigger was hit: %s.", triggerHit));
-                       if (triggerHit) {
-                               logger.info("Executing Action...");
-                               nextReaction.action().execute(trigger.output(nextReaction));
-                       }
-
-               }
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/reactor/engine/Starter.java b/src/main/java/net/pterodactylus/reactor/engine/Starter.java
deleted file mode 100644 (file)
index 0a0941e..0000000
+++ /dev/null
@@ -1,83 +0,0 @@
-/*
- * Reactor - Starter.java - Copyright © 2013 David Roden
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.reactor.engine;
-
-import net.pterodactylus.reactor.loader.ChainWatcher;
-import net.pterodactylus.reactor.states.StateManager;
-
-import com.lexicalscope.jewel.cli.CliFactory;
-import com.lexicalscope.jewel.cli.Option;
-
-/**
- * Reactor main starter class.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class Starter {
-
-       /**
-        * JVM main entry method.
-        *
-        * @param arguments
-        *            Command-line arguments
-        */
-       public static void main(String... arguments) {
-
-               /* parse command line. */
-               Parameters parameters = CliFactory.parseArguments(Parameters.class, arguments);
-
-               /* create the state manager. */
-               StateManager stateManager = new StateManager(parameters.getStateDirectory());
-
-               /* create the engine. */
-               Engine engine = new Engine(stateManager);
-
-               /* start a watcher. */
-               ChainWatcher chainWatcher = new ChainWatcher(engine, parameters.getChainDirectory());
-               chainWatcher.start();
-
-               /* start the engine. */
-               engine.start();
-       }
-
-       /**
-        * Definition of the command-line parameters.
-        *
-        * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
-        */
-       private static interface Parameters {
-
-               /**
-                * Returns the directory to watch for chains.
-                *
-                * @return The chain directory
-                */
-               @Option(defaultValue = "chains", shortName = "c", description = "The directory to watch for chains")
-               String getChainDirectory();
-
-               /**
-                * Returns the directory to store states in.
-                *
-                * @return The states directory
-                */
-               @Option(defaultValue = "states", shortName = "s", description = "The directory to store states in")
-               String getStateDirectory();
-
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/reactor/filters/EpisodeFilter.java b/src/main/java/net/pterodactylus/reactor/filters/EpisodeFilter.java
deleted file mode 100644 (file)
index 3f3b6a7..0000000
+++ /dev/null
@@ -1,102 +0,0 @@
-/*
- * Reactor - EpisodeFilter.java - Copyright © 2013 David Roden
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.reactor.filters;
-
-import static com.google.common.base.Preconditions.checkState;
-
-import java.util.LinkedHashMap;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-import net.pterodactylus.reactor.Filter;
-import net.pterodactylus.reactor.State;
-import net.pterodactylus.reactor.states.EpisodeState;
-import net.pterodactylus.reactor.states.EpisodeState.Episode;
-import net.pterodactylus.reactor.states.FailedState;
-import net.pterodactylus.reactor.states.TorrentState;
-import net.pterodactylus.reactor.states.TorrentState.TorrentFile;
-
-/**
- * {@link Filter} implementation that extracts {@link Episode} information from
- * the {@link TorrentFile}s contained in a {@link TorrentState}.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class EpisodeFilter implements Filter {
-
-       /** The pattern to parse episode information from the filename. */
-       private static Pattern episodePattern = Pattern.compile("S(\\d{2})E(\\d{2})|[^\\d](\\d{1,2})x(\\d{2})[^\\d]");
-
-       //
-       // FILTER METHODS
-       //
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       public State filter(State state) {
-               if (!state.success()) {
-                       return FailedState.from(state);
-               }
-               checkState(state instanceof TorrentState, "state is not a TorrentState but a %s!", state.getClass());
-
-               TorrentState torrentState = (TorrentState) state;
-               LinkedHashMap<Episode, Episode> episodes = new LinkedHashMap<Episode, Episode>();
-               for (TorrentFile torrentFile : torrentState) {
-                       Episode episode = extractEpisode(torrentFile);
-                       if (episode == null) {
-                               continue;
-                       }
-                       episodes.put(episode, episode);
-                       episode = episodes.get(episode);
-                       episode.addTorrentFile(torrentFile);
-               }
-
-               return new EpisodeState(episodes.values());
-       }
-
-       //
-       // STATIC METHODS
-       //
-
-       /**
-        * Extracts episode information from the given torrent file.
-        *
-        * @param torrentFile
-        *            The torrent file to extract the episode information from
-        * @return The extracted episode information, or {@code null} if no episode
-        *         information could be found
-        */
-       private static Episode extractEpisode(TorrentFile torrentFile) {
-               Matcher matcher = episodePattern.matcher(torrentFile.name());
-               if (!matcher.find()) {
-                       return null;
-               }
-               String seasonString = matcher.group(1);
-               String episodeString = matcher.group(2);
-               if ((seasonString == null) && (episodeString == null)) {
-                       seasonString = matcher.group(3);
-                       episodeString = matcher.group(4);
-               }
-               int season = Integer.valueOf(seasonString);
-               int episode = Integer.valueOf(episodeString);
-               return new Episode(season, episode);
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/reactor/filters/HtmlFilter.java b/src/main/java/net/pterodactylus/reactor/filters/HtmlFilter.java
deleted file mode 100644 (file)
index 83b961d..0000000
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
- * Reactor - HtmlFilter.java - Copyright © 2013 David Roden
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.reactor.filters;
-
-import static com.google.common.base.Preconditions.checkState;
-import net.pterodactylus.reactor.Filter;
-import net.pterodactylus.reactor.State;
-import net.pterodactylus.reactor.states.FailedState;
-import net.pterodactylus.reactor.states.HtmlState;
-import net.pterodactylus.reactor.states.HttpState;
-
-import org.jsoup.Jsoup;
-import org.jsoup.nodes.Document;
-
-/**
- * {@link Filter} that converts a {@link HttpState} into an {@link HtmlState}.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class HtmlFilter implements Filter {
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       public State filter(State state) {
-               if (!state.success()) {
-                       return FailedState.from(state);
-               }
-               checkState(state instanceof HttpState, "state is not a HttpState but a %s", state.getClass().getName());
-               Document document = Jsoup.parse(((HttpState) state).content(), ((HttpState) state).uri());
-               return new HtmlState(((HttpState) state).uri(), document);
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/reactor/filters/KickAssTorrentsFilter.java b/src/main/java/net/pterodactylus/reactor/filters/KickAssTorrentsFilter.java
deleted file mode 100644 (file)
index 9241ce7..0000000
+++ /dev/null
@@ -1,168 +0,0 @@
-/*
- * Reactor - KickAssTorrentsFilter.java - Copyright © 2013 David Roden
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.reactor.filters;
-
-import static com.google.common.base.Preconditions.checkState;
-
-import java.net.URI;
-import java.net.URISyntaxException;
-
-import net.pterodactylus.reactor.Filter;
-import net.pterodactylus.reactor.State;
-import net.pterodactylus.reactor.queries.HttpQuery;
-import net.pterodactylus.reactor.states.FailedState;
-import net.pterodactylus.reactor.states.HtmlState;
-import net.pterodactylus.reactor.states.TorrentState;
-import net.pterodactylus.reactor.states.TorrentState.TorrentFile;
-
-import org.jsoup.nodes.Document;
-import org.jsoup.nodes.Element;
-import org.jsoup.select.Elements;
-
-/**
- * {@link Filter} implementation that parses a {@link TorrentState} from an
- * {@link HtmlState} which was generated by a {@link HttpQuery} to
- * {@code kickasstorrents.ph}.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class KickAssTorrentsFilter implements Filter {
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       public State filter(State state) {
-               if (!state.success()) {
-                       return FailedState.from(state);
-               }
-               checkState(state instanceof HtmlState, "state is not an HtmlState but a %s", state.getClass().getName());
-
-               /* get result table. */
-               Document document = ((HtmlState) state).document();
-               Elements mainTable = document.select("table.data");
-               if (mainTable.isEmpty()) {
-                       /* no main table? */
-                       return new FailedState();
-               }
-
-               /* iterate over all rows. */
-               TorrentState torrentState = new TorrentState();
-               Elements dataRows = mainTable.select("tr:gt(0)");
-               for (Element dataRow : dataRows) {
-                       String name = extractName(dataRow);
-                       String size = extractSize(dataRow);
-                       String magnetUri = extractMagnetUri(dataRow);
-                       String downloadUri;
-                       int fileCount = extractFileCount(dataRow);
-                       int seedCount = extractSeedCount(dataRow);
-                       int leechCount = extractLeechCount(dataRow);
-                       try {
-                               downloadUri = new URI(((HtmlState) state).uri()).resolve(extractDownloadUri(dataRow)).toString();
-                               TorrentFile torrentFile = new TorrentFile(name, size, magnetUri, downloadUri, fileCount, seedCount, leechCount);
-                               torrentState.addTorrentFile(torrentFile);
-                       } catch (URISyntaxException use1) {
-                               /* ignore; if uri was wrong, we wouldn’t be here. */
-                       }
-               }
-
-               return torrentState;
-       }
-
-       //
-       // STATIC METHODS
-       //
-
-       /**
-        * Extracts the name from the given row.
-        *
-        * @param dataRow
-        *            The row to extract the name from
-        * @return The extracted name
-        */
-       private static String extractName(Element dataRow) {
-               return dataRow.select("div.torrentname a.normalgrey").text();
-       }
-
-       /**
-        * Extracts the size from the given row.
-        *
-        * @param dataRow
-        *            The row to extract the size from
-        * @return The extracted size
-        */
-       private static String extractSize(Element dataRow) {
-               return dataRow.select("td:eq(1)").text();
-       }
-
-       /**
-        * Extracts the magnet URI from the given row.
-        *
-        * @param dataRow
-        *            The row to extract the magnet URI from
-        * @return The extracted magnet URI
-        */
-       private static String extractMagnetUri(Element dataRow) {
-               return dataRow.select("a.imagnet").attr("href");
-       }
-
-       /**
-        * Extracts the download URI from the given row.
-        *
-        * @param dataRow
-        *            The row to extract the download URI from
-        * @return The extracted download URI
-        */
-       private static String extractDownloadUri(Element dataRow) {
-               return dataRow.select("a.idownload:not(.partner1Button)").attr("href");
-       }
-
-       /**
-        * Extracts the file count from the given row.
-        *
-        * @param dataRow
-        *            The row to extract the file count from
-        * @return The extracted file count
-        */
-       private static int extractFileCount(Element dataRow) {
-               return Integer.valueOf(dataRow.select("td:eq(2)").text());
-       }
-
-       /**
-        * Extracts the seed count from the given row.
-        *
-        * @param dataRow
-        *            The row to extract the seed count from
-        * @return The extracted seed count
-        */
-       private static int extractSeedCount(Element dataRow) {
-               return Integer.valueOf(dataRow.select("td:eq(4)").text());
-       }
-
-       /**
-        * Extracts the leech count from the given row.
-        *
-        * @param dataRow
-        *            The row to extract the leech count from
-        * @return The extracted leech count
-        */
-       private static int extractLeechCount(Element dataRow) {
-               return Integer.valueOf(dataRow.select("td:eq(5)").text());
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/reactor/loader/Chain.java b/src/main/java/net/pterodactylus/reactor/loader/Chain.java
deleted file mode 100644 (file)
index 8c98e50..0000000
+++ /dev/null
@@ -1,314 +0,0 @@
-/*
- * Reactor - Chain.java - Copyright © 2013 David Roden
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.reactor.loader;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import com.fasterxml.jackson.annotation.JsonProperty;
-
-/**
- * Model for chain definitions.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class Chain {
-
-       /**
-        * Parameter model.
-        *
-        * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
-        */
-       public static class Parameter {
-
-               /** The name of the parameter. */
-               @JsonProperty
-               private String name;
-
-               /** The value of the parameter. */
-               @JsonProperty
-               private String value;
-
-               /**
-                * Returns the name of the parameter.
-                *
-                * @return The name of the parameter
-                */
-               public String name() {
-                       return name;
-               }
-
-               /**
-                * Returns the value of the parameter.
-                *
-                * @return The value of the parameter
-                */
-               public String value() {
-                       return value;
-               }
-
-               /**
-                * {@inheritDoc}
-                */
-               @Override
-               public int hashCode() {
-                       int hashCode = 0;
-                       hashCode ^= name.hashCode();
-                       hashCode ^= value.hashCode();
-                       return hashCode;
-               }
-
-               /**
-                * {@inheritDoc}
-                */
-               @Override
-               public boolean equals(Object object) {
-                       if (!(object instanceof Parameter)) {
-                               return false;
-                       }
-                       Parameter parameter = (Parameter) object;
-                       if (!name.equals(parameter.name)) {
-                               return false;
-                       }
-                       if (!value.equals(parameter.value)) {
-                               return false;
-                       }
-                       return true;
-               }
-
-       }
-
-       /**
-        * Defines a part of a chain.
-        *
-        * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
-        */
-       public static class Part {
-
-               /** The class name of the part. */
-               @JsonProperty(value = "class")
-               private String name;
-
-               /** The parameters of the part. */
-               @JsonProperty
-               private List<Parameter> parameters = new ArrayList<Parameter>();
-
-               /**
-                * Returns the name of the part’s class.
-                *
-                * @return The name of the part’s class
-                */
-               public String name() {
-                       return name;
-               }
-
-               /**
-                * Returns the parameters of the part.
-                *
-                * @return The parameters of the part
-                */
-               public List<Parameter> parameters() {
-                       return parameters;
-               }
-
-               /**
-                * {@inheritDoc}
-                */
-               @Override
-               public int hashCode() {
-                       int hashCode = 0;
-                       hashCode ^= name.hashCode();
-                       for (Parameter parameter : parameters) {
-                               hashCode ^= parameter.hashCode();
-                       }
-                       return hashCode;
-               }
-
-               /**
-                * {@inheritDoc}
-                */
-               @Override
-               public boolean equals(Object object) {
-                       if (!(object instanceof Part)) {
-                               return false;
-                       }
-                       Part part = (Part) object;
-                       if (!name.equals(part.name)) {
-                               return false;
-                       }
-                       if (parameters.size() != part.parameters.size()) {
-                               return false;
-                       }
-                       for (int parameterIndex = 0; parameterIndex < parameters.size(); ++parameterIndex) {
-                               if (!parameters.get(parameterIndex).equals(part.parameters.get(parameterIndex))) {
-                                       return false;
-                               }
-                       }
-                       return true;
-               }
-
-       }
-
-       /** Whether this chain is enabled. */
-       @JsonProperty
-       private boolean enabled;
-
-       /** The name of the chain. */
-       @JsonProperty
-       private String name;
-
-       /** The query of the chain. */
-       @JsonProperty
-       private Part query;
-
-       /** The filters of the chain. */
-       @JsonProperty
-       private List<Part> filters = new ArrayList<Part>();
-
-       /** The trigger of the chain. */
-       @JsonProperty
-       private Part trigger;
-
-       /** The action of the chain. */
-       @JsonProperty
-       private Part action;
-
-       /** Interval between updates (in seconds). */
-       @JsonProperty
-       private int updateInterval;
-
-       /**
-        * Returns whether this chain is enabled.
-        *
-        * @return {@code true} if this chain is enabled, {@code false} otherwise
-        */
-       public boolean enabled() {
-               return enabled;
-       }
-
-       /**
-        * Returns the name of the chain.
-        *
-        * @return The name of the chain
-        */
-       public String name() {
-               return name;
-       }
-
-       /**
-        * Returns the query of this chain.
-        *
-        * @return The query of this chain
-        */
-       public Part query() {
-               return query;
-       }
-
-       /**
-        * Returns the filters of this chain.
-        *
-        * @return The filters of this chain
-        */
-       public List<Part> filters() {
-               return filters;
-       }
-
-       /**
-        * Returns the trigger of this chain.
-        *
-        * @return The trigger of this chain
-        */
-       public Part trigger() {
-               return trigger;
-       }
-
-       /**
-        * Returns the action of this chain.
-        *
-        * @return The action of this chain
-        */
-       public Part action() {
-               return action;
-       }
-
-       /**
-        * Returns the update interval of the chain.
-        *
-        * @return The update interval (in seconds)
-        */
-       public int updateInterval() {
-               return updateInterval;
-       }
-
-       //
-       // OBJECT METHODS
-       //
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       public int hashCode() {
-               int hashCode = 0;
-               hashCode ^= name.hashCode();
-               hashCode ^= query.hashCode();
-               for (Part filter : filters) {
-                       hashCode ^= filter.hashCode();
-               }
-               hashCode ^= trigger.hashCode();
-               hashCode ^= action.hashCode();
-               hashCode ^= updateInterval;
-               return hashCode;
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       public boolean equals(Object object) {
-               if (!(object instanceof Chain)) {
-                       return false;
-               }
-               Chain chain = (Chain) object;
-               if (!name.equals(chain.name)) {
-                       return false;
-               }
-               if (!query.equals(chain.query)) {
-                       return false;
-               }
-               if (filters.size() != chain.filters.size()) {
-                       return false;
-               }
-               for (int filterIndex = 0; filterIndex < filters.size(); ++filterIndex) {
-                       if (!filters.get(filterIndex).equals(chain.filters.get(filterIndex))) {
-                               return false;
-                       }
-               }
-               if (!trigger.equals(chain.trigger)) {
-                       return false;
-               }
-               if (!action.equals(chain.action)) {
-                       return false;
-               }
-               if (updateInterval != chain.updateInterval) {
-                       return false;
-               }
-               return true;
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/reactor/loader/ChainWatcher.java b/src/main/java/net/pterodactylus/reactor/loader/ChainWatcher.java
deleted file mode 100644 (file)
index 3d30f26..0000000
+++ /dev/null
@@ -1,234 +0,0 @@
-/*
- * Reactor - ChainWatcher.java - Copyright © 2013 David Roden
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.reactor.loader;
-
-import java.io.File;
-import java.io.FilenameFilter;
-import java.io.IOException;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Map;
-import java.util.Map.Entry;
-import java.util.Set;
-import java.util.concurrent.TimeUnit;
-
-import net.pterodactylus.reactor.Reaction;
-import net.pterodactylus.reactor.engine.Engine;
-import net.pterodactylus.reactor.loader.Chain.Parameter;
-import net.pterodactylus.reactor.loader.Chain.Part;
-
-import org.apache.log4j.Logger;
-
-import com.fasterxml.jackson.core.JsonParseException;
-import com.fasterxml.jackson.databind.JsonMappingException;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.google.common.base.Predicate;
-import com.google.common.collect.Maps;
-import com.google.common.util.concurrent.AbstractExecutionThreadService;
-import com.google.common.util.concurrent.Uninterruptibles;
-
-/**
- * Watches a directory for chain configuration files and loads and unloads
- * {@link Reaction}s from the {@link Engine}.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class ChainWatcher extends AbstractExecutionThreadService {
-
-       /** The logger. */
-       private static final Logger logger = Logger.getLogger(ChainWatcher.class);
-
-       /** The JSON object mapper. */
-       private static final ObjectMapper objectMapper = new ObjectMapper();
-
-       /** The reaction loader. */
-       private final ReactionLoader reactionLoader = new ReactionLoader();
-
-       /** The engine to load reactions with. */
-       private final Engine engine;
-
-       /** The directory to watch for chain configuration files. */
-       private final String directory;
-
-       /**
-        * Creates a new chain watcher.
-        *
-        * @param engine
-        *            The engine to load reactions with
-        * @param directory
-        *            The directory to watch
-        */
-       public ChainWatcher(Engine engine, String directory) {
-               this.engine = engine;
-               this.directory = directory;
-       }
-
-       //
-       // ABSTRACTEXECUTIONTHREADSERVICE METHODS
-       //
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       protected void run() throws Exception {
-
-               /* loaded chains. */
-               final Map<String, Chain> loadedChains = new HashMap<String, Chain>();
-
-               while (isRunning()) {
-
-                       /* check if directory is there. */
-                       File directoryFile = new File(directory);
-                       if (!directoryFile.exists() || !directoryFile.isDirectory() || !directoryFile.canRead()) {
-                               Uninterruptibles.sleepUninterruptibly(1, TimeUnit.SECONDS);
-                               continue;
-                       }
-
-                       /* list all files, scan for configuration files. */
-                       logger.debug(String.format("Scanning %s...", directory));
-                       File[] configurationFiles = directoryFile.listFiles(new FilenameFilter() {
-
-                               @Override
-                               public boolean accept(File dir, String name) {
-                                       return name.endsWith(".json");
-                               }
-                       });
-                       logger.debug(String.format("Found %d configuration file(s), parsing...", configurationFiles.length));
-
-                       /* now parse all XML files. */
-                       Map<String, Chain> chains = new HashMap<String, Chain>();
-                       for (File configurationFile : configurationFiles) {
-
-                               /* parse XML file. */
-                               Chain chain = parseConfigurationFile(configurationFile);
-                               if (chain == null) {
-                                       logger.warn(String.format("Could not parse %s.", configurationFile));
-                                       continue;
-                               }
-
-                               /* dump chain */
-                               logger.debug(String.format(" Enabled: %s", chain.enabled()));
-
-                               logger.debug(String.format(" Query: %s", chain.query().name()));
-                               for (Parameter parameter : chain.query().parameters()) {
-                                       logger.debug(String.format("  Parameter: %s=%s", parameter.name(), parameter.value()));
-                               }
-                               for (Part filter : chain.filters()) {
-                                       logger.debug(String.format(" Filter: %s", filter.name()));
-                                       for (Parameter parameter : filter.parameters()) {
-                                               logger.debug(String.format("  Parameter: %s=%s", parameter.name(), parameter.value()));
-                                       }
-                               }
-                               logger.debug(String.format(" Trigger: %s", chain.trigger().name()));
-                               for (Parameter parameter : chain.trigger().parameters()) {
-                                       logger.debug(String.format("  Parameter: %s=%s", parameter.name(), parameter.value()));
-                               }
-                               logger.debug(String.format(" Action: %s", chain.action().name()));
-                               for (Parameter parameter : chain.action().parameters()) {
-                                       logger.debug(String.format("  Parameter: %s=%s", parameter.name(), parameter.value()));
-                               }
-
-                               chains.put(getReactionName(configurationFile.getName()), chain);
-                       }
-
-                       /* filter enabled chains. */
-                       Map<String, Chain> enabledChains = Maps.filterEntries(chains, new Predicate<Entry<String, Chain>>() {
-
-                               @Override
-                               public boolean apply(Entry<String, Chain> chainEntry) {
-                                       return chainEntry.getValue().enabled();
-                               }
-                       });
-                       logger.debug(String.format("Found %d enabled Chain(s).", enabledChains.size()));
-
-                       /* check for removed chains. */
-                       Set<String> chainsToRemove = new HashSet<String>();
-                       for (Entry<String, Chain> loadedChain : loadedChains.entrySet()) {
-
-                               /* skip chains that still exist. */
-                               if (enabledChains.containsKey(loadedChain.getKey())) {
-                                       continue;
-                               }
-
-                               logger.info(String.format("Removing Chain: %s", loadedChain.getKey()));
-                               engine.removeReaction(loadedChain.getKey());
-                               chainsToRemove.add(loadedChain.getKey());
-                       }
-
-                       /* remove removed chains from loaded chains. */
-                       for (String reactionName : chainsToRemove) {
-                               loadedChains.remove(reactionName);
-                       }
-
-                       /* check for new chains. */
-                       for (Entry<String, Chain> enabledChain : enabledChains.entrySet()) {
-
-                               /* skip already loaded chains. */
-                               if (loadedChains.containsValue(enabledChain.getValue())) {
-                                       continue;
-                               }
-
-                               logger.info(String.format("Loading new Chain: %s", enabledChain.getKey()));
-
-                               Reaction reaction = reactionLoader.loadReaction(enabledChain.getValue());
-                               engine.addReaction(enabledChain.getKey(), reaction);
-                               loadedChains.put(enabledChain.getKey(), enabledChain.getValue());
-                       }
-
-                       /* wait before checking again. */
-                       Uninterruptibles.sleepUninterruptibly(5, TimeUnit.SECONDS);
-               }
-       }
-
-       //
-       // STATIC METHODS
-       //
-
-       /**
-        * Parses the given configuration file into a {@link Chain}.
-        *
-        * @param configurationFile
-        *            The configuration file to parse
-        * @return The parsed chain
-        */
-       private static Chain parseConfigurationFile(File configurationFile) {
-               try {
-                       return objectMapper.readValue(configurationFile, Chain.class);
-               } catch (JsonParseException jpe1) {
-                       logger.warn(String.format("Could not parse %s.", configurationFile), jpe1);
-               } catch (JsonMappingException jme1) {
-                       logger.warn(String.format("Could not parse %s.", configurationFile), jme1);
-               } catch (IOException ioe1) {
-                       logger.info(String.format("Could not read %s.", configurationFile));
-               }
-               return null;
-       }
-
-       /**
-        * Extracts the name of the reaction from the given filename.
-        *
-        * @param filename
-        *            The filename to extract the reaction name from
-        * @return The name of the reaction
-        */
-       private static String getReactionName(String filename) {
-               return (filename.lastIndexOf(".") > -1) ? filename.substring(0, filename.lastIndexOf(".")) : filename;
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/reactor/loader/LoaderException.java b/src/main/java/net/pterodactylus/reactor/loader/LoaderException.java
deleted file mode 100644 (file)
index bffff4a..0000000
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
- * Reactor - LoaderException.java - Copyright © 2013 David Roden
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.reactor.loader;
-
-/**
- * Exception that signals a problem when loading chain XML files.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class LoaderException extends Exception {
-
-       /**
-        * Creates a new loader exception.
-        */
-       public LoaderException() {
-               super();
-       }
-
-       /**
-        * Creates a new loader exception.
-        *
-        * @param message
-        *            The message of the exception
-        */
-       public LoaderException(String message) {
-               super(message);
-       }
-
-       /**
-        * Creates a new loader exception.
-        *
-        * @param throwable
-        *            The root cause
-        */
-       public LoaderException(Throwable throwable) {
-               super(throwable);
-       }
-
-       /**
-        * Creates a new loader exception.
-        *
-        * @param message
-        *            The message of the exception
-        * @param throwable
-        *            The root cause
-        */
-       public LoaderException(String message, Throwable throwable) {
-               super(message, throwable);
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/reactor/loader/ReactionLoader.java b/src/main/java/net/pterodactylus/reactor/loader/ReactionLoader.java
deleted file mode 100644 (file)
index a2c29aa..0000000
+++ /dev/null
@@ -1,177 +0,0 @@
-/*
- * Reactor - Loader.java - Copyright © 2013 David Roden
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.reactor.loader;
-
-import java.lang.reflect.Constructor;
-import java.lang.reflect.InvocationTargetException;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.concurrent.TimeUnit;
-
-import net.pterodactylus.reactor.Action;
-import net.pterodactylus.reactor.Filter;
-import net.pterodactylus.reactor.Query;
-import net.pterodactylus.reactor.Reaction;
-import net.pterodactylus.reactor.Trigger;
-import net.pterodactylus.reactor.loader.Chain.Parameter;
-import net.pterodactylus.reactor.loader.Chain.Part;
-
-/**
- * Creates {@link Reaction}s from {@link Chain}s.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class ReactionLoader {
-
-       /**
-        * Creates a {@link Reaction} from the given {@link Chain}.
-        *
-        * @param chain
-        *            The chain to create a reaction from
-        * @return The created reaction
-        * @throws LoaderException
-        *             if a class can not be loaded
-        */
-       @SuppressWarnings("static-method")
-       public Reaction loadReaction(Chain chain) throws LoaderException {
-
-               /* check if chain is enabled. */
-               if (!chain.enabled()) {
-                       throw new IllegalArgumentException("Chain is not enabled.");
-               }
-
-               /* create query. */
-               Query query = createObject(chain.query().name(), "net.pterodactylus.reactor.queries", extractParameters(chain.query().parameters()));
-
-               /* create filters. */
-               List<Filter> filters = new ArrayList<Filter>();
-               for (Part filterPart : chain.filters()) {
-                       filters.add(ReactionLoader.<Filter> createObject(filterPart.name(), "net.pterodactylus.reactor.filters", extractParameters(filterPart.parameters())));
-               }
-
-               /* create trigger. */
-               Trigger trigger = createObject(chain.trigger().name(), "net.pterodactylus.reactor.triggers", extractParameters(chain.trigger().parameters()));
-
-               /* create action. */
-               Action action = createObject(chain.action().name(), "net.pterodactylus.reactor.actions", extractParameters(chain.action().parameters()));
-
-               return new Reaction(chain.name(), query, filters, trigger, action).setUpdateInterval(TimeUnit.SECONDS.toMillis(chain.updateInterval()));
-       }
-
-       //
-       // STATIC METHODS
-       //
-
-       /**
-        * Extracts all parameter values from the given parameters.
-        *
-        * @param parameters
-        *            The parameters to extract the values from
-        * @return The extracted values
-        */
-       private static List<String> extractParameters(List<Parameter> parameters) {
-               List<String> parameterValues = new ArrayList<String>();
-
-               for (Parameter parameter : parameters) {
-                       parameterValues.add(parameter.value());
-               }
-
-               return parameterValues;
-       }
-
-       /**
-        * Creates a new object.
-        * <p>
-        * First, {@code className} is used to try to load a {@link Class} with that
-        * name. If that fails, {@code packageName} is prepended to the class name.
-        * If no class can be found, a {@link LoaderException} will be thrown.
-        * <p>
-        * If a class could be located using the described method, a constructor
-        * will be searched that has the same number of {@link String} parameters as
-        * the given parameters. The parameters from the given parameters are then
-        * used in a constructor call to create the new object.
-        *
-        * @param className
-        *            The name of the class
-        * @param packageName
-        *            The optional name of the package to prepend
-        * @param parameters
-        *            The parameters for the constructor call
-        * @return The created object
-        * @throws LoaderException
-        *             if the object can not be created
-        */
-       @SuppressWarnings("unchecked")
-       private static <T> T createObject(String className, String packageName, List<String> parameters) throws LoaderException {
-
-               /* try to load class without package name. */
-               Class<?> objectClass = null;
-               try {
-                       objectClass = Class.forName(className);
-               } catch (ClassNotFoundException cnfe1) {
-                       /* ignore, we’ll try again. */
-               }
-
-               if (objectClass == null) {
-                       try {
-                               objectClass = Class.forName(packageName + "." + className);
-                       } catch (ClassNotFoundException cnfe1) {
-                               /* okay, now we need to throw. */
-                               throw new LoaderException(String.format("Could find neither class “%s” nor class “%s.”", className, packageName + "." + className), cnfe1);
-                       }
-               }
-
-               /* locate an eligible constructor. */
-               Constructor<?> wantedConstructor = null;
-               for (Constructor<?> constructor : objectClass.getConstructors()) {
-                       Class<?>[] parameterTypes = constructor.getParameterTypes();
-                       if (parameterTypes.length != parameters.size()) {
-                               continue;
-                       }
-                       boolean compatibleTypes = true;
-                       for (Class<?> parameterType : parameterTypes) {
-                               if (parameterType != String.class) {
-                                       compatibleTypes = false;
-                                       break;
-                               }
-                       }
-                       if (!compatibleTypes) {
-                               continue;
-                       }
-                       wantedConstructor = constructor;
-               }
-
-               if (wantedConstructor == null) {
-                       throw new LoaderException("Could not find eligible constructor.");
-               }
-
-               try {
-                       return (T) wantedConstructor.newInstance(parameters.toArray());
-               } catch (IllegalArgumentException iae1) {
-                       throw new LoaderException("Could not invoke constructor.", iae1);
-               } catch (InstantiationException ie1) {
-                       throw new LoaderException("Could not invoke constructor.", ie1);
-               } catch (IllegalAccessException iae1) {
-                       throw new LoaderException("Could not invoke constructor.", iae1);
-               } catch (InvocationTargetException ite1) {
-                       throw new LoaderException("Could not invoke constructor.", ite1);
-               }
-
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/reactor/output/DefaultOutput.java b/src/main/java/net/pterodactylus/reactor/output/DefaultOutput.java
deleted file mode 100644 (file)
index 36d836c..0000000
+++ /dev/null
@@ -1,85 +0,0 @@
-/*
- * Reactor - DefaultOutput.java - Copyright © 2013 David Roden
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.reactor.output;
-
-import java.util.Map;
-
-import com.google.common.collect.Maps;
-
-/**
- * {@link Output} implementation that stores texts for arbitrary MIME types.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class DefaultOutput implements Output {
-
-       /** The summary of the output. */
-       private final String summary;
-
-       /** The texts for the different MIME types. */
-       private final Map<String, String> mimeTypeTexts = Maps.newHashMap();
-
-       /**
-        * Creates a new default output.
-        *
-        * @param summary
-        *            The summary of the output
-        */
-       public DefaultOutput(String summary) {
-               this.summary = summary;
-       }
-
-       //
-       // ACTIONS
-       //
-
-       /**
-        * Adds the given text for the given MIME type.
-        *
-        * @param mimeType
-        *            The MIME type to add the text for
-        * @param text
-        *            The text to add
-        * @return This default output
-        */
-       public DefaultOutput addText(String mimeType, String text) {
-               mimeTypeTexts.put(mimeType, text);
-               return this;
-       }
-
-       //
-       // OUTPUT METHODS
-       //
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       public String summary() {
-               return summary;
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       public String text(String mimeType, int maxLength) {
-               return mimeTypeTexts.get(mimeType);
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/reactor/output/Output.java b/src/main/java/net/pterodactylus/reactor/output/Output.java
deleted file mode 100644 (file)
index b3e26c7..0000000
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- * Reactor - Output.java - Copyright © 2013 David Roden
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.reactor.output;
-
-import net.pterodactylus.reactor.Trigger;
-
-/**
- * Defines the output of a {@link Trigger}. As different output has to be
- * generated for different media, the {@link #text(String, int)} method takes as
- * an argument the MIME type of the desired output.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public interface Output {
-
-       /**
-        * Returns a short summary that can be included e. g. in the subject of an
-        * email.
-        *
-        * @return A short summary of the output
-        */
-       String summary();
-
-       /**
-        * Returns the text for the given MIME type and the given maximum length.
-        * Note that the maximum length does not need to be enforced at all costs;
-        * implementation are free to return texts longer than the given number of
-        * characters.
-        *
-        * @param mimeType
-        *            The MIME type of the text (“text/plain” and “text/html” should
-        *            be supported by all {@link Trigger}s)
-        * @param maxLength
-        *            The maximum length of the returned text (may be &lt; {@code 0}
-        *            to indicate no length restriction)
-        * @return The text for the given MIME type, or {@code null} if there is no
-        *         text defined for the given MIME type
-        */
-       String text(String mimeType, int maxLength);
-
-}
diff --git a/src/main/java/net/pterodactylus/reactor/package-info.java b/src/main/java/net/pterodactylus/reactor/package-info.java
deleted file mode 100644 (file)
index 708319c..0000000
+++ /dev/null
@@ -1,25 +0,0 @@
-/**
- * Reactor main definitions.
- * <p>
- * A {@link net.pterodactylus.reactor.Reaction} consists of three different
- * elements: a {@link net.pterodactylus.reactor.Query}, a
- * {@link net.pterodactylus.reactor.Trigger}, and an
- * {@link net.pterodactylus.reactor.Action}.
- * <p>
- * A {@code Query} retrieves the current state of a system; this can simply be
- * the current state of a local file, or it can be the last tweet of a certain
- * Twitter account, or it can be anything inbetween, or something completely
- * different.
- * <p>
- * After a {@code Query} retrieved the current
- * {@link net.pterodactylus.reactor.State} of a system, this state and the
- * previously retrieved state are handed in to a {@code Trigger}. The trigger
- * then decides whether the state of the system can be considered a change.
- * <p>
- * If a system has been found to trigger, an {@code Action} is executed. It
- * performs arbitrary actions and can use both the current state and the
- * previous state to define that action.
- */
-
-package net.pterodactylus.reactor;
-
diff --git a/src/main/java/net/pterodactylus/reactor/queries/FileQuery.java b/src/main/java/net/pterodactylus/reactor/queries/FileQuery.java
deleted file mode 100644 (file)
index ac0e895..0000000
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * Reactor - FileQuery.java - Copyright © 2013 David Roden
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.reactor.queries;
-
-import static com.google.common.base.Preconditions.checkNotNull;
-
-import java.io.File;
-
-import net.pterodactylus.reactor.Query;
-import net.pterodactylus.reactor.State;
-import net.pterodactylus.reactor.states.FileState;
-
-/**
- * Queries the filesystem about a file.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class FileQuery implements Query {
-
-       /** The name of the file to query. */
-       private final String filename;
-
-       /**
-        * Creates a new file query.
-        *
-        * @param filename
-        *            The name of the file to query
-        */
-       public FileQuery(String filename) {
-               this.filename = checkNotNull(filename, "filename must not be null");
-       }
-
-       //
-       // QUERY METHODS
-       //
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       public State state() {
-               File file = new File(filename);
-               if (!file.exists()) {
-                       return new FileState(false, false, -1, -1);
-               }
-               if (!file.canRead()) {
-                       return new FileState(true, false, -1, -1);
-               }
-               return new FileState(true, true, file.length(), file.lastModified());
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/reactor/queries/HttpQuery.java b/src/main/java/net/pterodactylus/reactor/queries/HttpQuery.java
deleted file mode 100644 (file)
index 2cec118..0000000
+++ /dev/null
@@ -1,92 +0,0 @@
-/*
- * Reactor - HttpQuery.java - Copyright © 2013 David Roden
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.reactor.queries;
-
-import java.io.IOException;
-import java.io.InputStreamReader;
-
-import net.pterodactylus.reactor.Query;
-import net.pterodactylus.reactor.State;
-import net.pterodactylus.reactor.states.FailedState;
-import net.pterodactylus.reactor.states.HttpState;
-
-import org.apache.http.HttpEntity;
-import org.apache.http.HttpResponse;
-import org.apache.http.HttpStatus;
-import org.apache.http.client.methods.HttpGet;
-import org.apache.http.client.protocol.ResponseContentEncoding;
-import org.apache.http.impl.client.DefaultHttpClient;
-import org.apache.http.util.EntityUtils;
-
-import com.google.common.io.Closeables;
-
-/**
- * {@link Query} that performs an HTTP GET request to a fixed uri.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class HttpQuery implements Query {
-
-       /** The uri to request. */
-       private final String uri;
-
-       /**
-        * Creates a new HTTP query.
-        *
-        * @param uri
-        *            The uri to request
-        */
-       public HttpQuery(String uri) {
-               this.uri = uri;
-       }
-
-       //
-       // QUERY METHODS
-       //
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       @SuppressWarnings("deprecation")
-       public State state() {
-               DefaultHttpClient httpClient = new DefaultHttpClient();
-               httpClient.addResponseInterceptor(new ResponseContentEncoding());
-               HttpGet get = new HttpGet(uri);
-
-               InputStreamReader inputStreamReader = null;
-               try {
-                       /* make request. */
-                       get.addHeader("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/536.11 (KHTML, like Gecko) Ubuntu/12.04 Chromium/20.0.1132.47 Chrome/20.0.1132.47 Safari/536.11");
-                       HttpResponse response = httpClient.execute(get);
-                       if (response.getStatusLine().getStatusCode() != HttpStatus.SC_OK) {
-                               return new FailedState();
-                       }
-                       HttpEntity entity = response.getEntity();
-
-                       /* yay, done! */
-                       return new HttpState(uri, response.getStatusLine().getStatusCode(), entity.getContentType().getValue(), EntityUtils.toByteArray(entity));
-
-               } catch (IOException ioe1) {
-                       return new FailedState(ioe1);
-               } finally {
-                       Closeables.closeQuietly(inputStreamReader);
-               }
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/reactor/states/AbstractState.java b/src/main/java/net/pterodactylus/reactor/states/AbstractState.java
deleted file mode 100644 (file)
index 6d7a6ea..0000000
+++ /dev/null
@@ -1,135 +0,0 @@
-/*
- * Reactor - AbstractState.java - Copyright © 2013 David Roden
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.reactor.states;
-
-import net.pterodactylus.reactor.State;
-
-import com.fasterxml.jackson.annotation.JsonProperty;
-import com.fasterxml.jackson.annotation.JsonTypeInfo;
-
-/**
- * Abstract implementation of a {@link State} that knows about the basic
- * attributes of a {@link State}.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY, property = "@class")
-public abstract class AbstractState implements State {
-
-       /** The time of this state. */
-       @JsonProperty
-       private final long time;
-
-       /** Whether the state was successfully retrieved. */
-       private final boolean success;
-
-       /** The optional exception that occured while retrieving the state. */
-       private final Throwable exception;
-
-       /** The number of consecutive failures. */
-       @JsonProperty
-       private int failCount;
-
-       /**
-        * Creates a new successful state.
-        */
-       protected AbstractState() {
-               this(true);
-       }
-
-       /**
-        * Creates a new state.
-        *
-        * @param success
-        *            {@code true} if the state is successful, {@code false}
-        *            otherwise
-        */
-       protected AbstractState(boolean success) {
-               this(success, null);
-       }
-
-       /**
-        * Creates a new non-successful state with the given exception.
-        *
-        * @param exception
-        *            The exception that occured while retrieving the state
-        */
-       protected AbstractState(Throwable exception) {
-               this(false, exception);
-       }
-
-       /**
-        * Creates a new state.
-        *
-        * @param success
-        *            {@code true} if the state is successful, {@code false}
-        *            otherwise
-        * @param exception
-        *            The exception that occured while retrieving the state
-        */
-       protected AbstractState(boolean success, Throwable exception) {
-               this.time = System.currentTimeMillis();
-               this.success = success;
-               this.exception = exception;
-       }
-
-       //
-       // STATE METHODS
-       //
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       public long time() {
-               return time;
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       public boolean success() {
-               return success;
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       public int failCount() {
-               return failCount;
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       public void setFailCount(int failCount) {
-               this.failCount = failCount;
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       public Throwable exception() {
-               return exception;
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/reactor/states/EpisodeState.java b/src/main/java/net/pterodactylus/reactor/states/EpisodeState.java
deleted file mode 100644 (file)
index 1c120d6..0000000
+++ /dev/null
@@ -1,236 +0,0 @@
-/*
- * Reactor - EpisodeState.java - Copyright © 2013 David Roden
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.reactor.states;
-
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Iterator;
-import java.util.List;
-
-import net.pterodactylus.reactor.State;
-import net.pterodactylus.reactor.filters.EpisodeFilter;
-import net.pterodactylus.reactor.states.EpisodeState.Episode;
-import net.pterodactylus.reactor.states.TorrentState.TorrentFile;
-
-import com.fasterxml.jackson.annotation.JsonProperty;
-
-/**
- * {@link State} implementation that stores episodes of TV shows, parsed via
- * {@link EpisodeFilter} from a previous {@link TorrentState}.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class EpisodeState extends AbstractState implements Iterable<Episode> {
-
-       /** The episodes found in the current request. */
-       @JsonProperty
-       private final List<Episode> episodes = new ArrayList<Episode>();
-
-       /**
-        * 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
-        */
-       public EpisodeState(Collection<Episode> episodes) {
-               this.episodes.addAll(episodes);
-       }
-
-       //
-       // ACCESSORS
-       //
-
-       /**
-        * Returns all episodes contained in this state.
-        *
-        * @return The episodes of this state
-        */
-       public Collection<Episode> episodes() {
-               return Collections.unmodifiableCollection(episodes);
-       }
-
-       //
-       // ITERABLE INTERFACE
-       //
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       public Iterator<Episode> iterator() {
-               return episodes.iterator();
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       public String toString() {
-               return String.format("%s[episodes=%s]", getClass().getSimpleName(), episodes);
-       }
-
-       /**
-        * Stores attributes for an episode.
-        *
-        * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
-        */
-       public static class Episode implements Iterable<TorrentFile> {
-
-               /** The season of the episode. */
-               @JsonProperty
-               private final int season;
-
-               /** The number of the episode. */
-               @JsonProperty
-               private final int episode;
-
-               /** The torrent files for this episode. */
-               @JsonProperty
-               private final List<TorrentFile> torrentFiles = new ArrayList<TorrentFile>();
-
-               /**
-                * No-arg constructor for deserialization.
-                */
-               @SuppressWarnings("unused")
-               private Episode() {
-                       this(0, 0);
-               }
-
-               /**
-                * Creates a new episode.
-                *
-                * @param season
-                *            The season of the episode
-                * @param episode
-                *            The number of the episode
-                */
-               public Episode(int season, int episode) {
-                       this.season = season;
-                       this.episode = episode;
-               }
-
-               //
-               // ACCESSORS
-               //
-
-               /**
-                * Returns the season of this episode.
-                *
-                * @return The season of this episode
-                */
-               public int season() {
-                       return season;
-               }
-
-               /**
-                * Returns the number of this episode.
-                *
-                * @return The number of this episode
-                */
-               public int episode() {
-                       return episode;
-               }
-
-               /**
-                * Returns the torrent files of this episode.
-                *
-                * @return The torrent files of this episode
-                */
-               public Collection<TorrentFile> torrentFiles() {
-                       return torrentFiles;
-               }
-
-               /**
-                * Returns the identifier of this episode.
-                *
-                * @return The identifier of this episode
-                */
-               public String identifier() {
-                       return String.format("S%02dE%02d", season, episode);
-               }
-
-               //
-               // ACTIONS
-               //
-
-               /**
-                * Adds the given torrent file to this episode.
-                *
-                * @param torrentFile
-                *            The torrent file to add
-                */
-               public void addTorrentFile(TorrentFile torrentFile) {
-                       torrentFiles.add(torrentFile);
-               }
-
-               //
-               // ITERABLE METHODS
-               //
-
-               /**
-                * {@inheritDoc}
-                */
-               @Override
-               public Iterator<TorrentFile> iterator() {
-                       return torrentFiles.iterator();
-               }
-
-               //
-               // OBJECT METHODS
-               //
-
-               /**
-                * {@inheritDoc}
-                */
-               @Override
-               public int hashCode() {
-                       return season * 65536 + episode;
-               }
-
-               /**
-                * {@inheritDoc}
-                */
-               @Override
-               public boolean equals(Object obj) {
-                       if (!(obj instanceof Episode)) {
-                               return false;
-                       }
-                       Episode episode = (Episode) obj;
-                       return (season == episode.season) && (this.episode == episode.episode);
-               }
-
-               /**
-                * {@inheritDoc}
-                */
-               @Override
-               public String toString() {
-                       return String.format("%s[season=%d,episode=%d,torrentFiles=%s]", getClass().getSimpleName(), season, episode, torrentFiles);
-               }
-
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/reactor/states/FailedState.java b/src/main/java/net/pterodactylus/reactor/states/FailedState.java
deleted file mode 100644 (file)
index cfcbeef..0000000
+++ /dev/null
@@ -1,81 +0,0 @@
-/*
- * Reactor - FailedState.java - Copyright © 2013 David Roden
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.reactor.states;
-
-import net.pterodactylus.reactor.State;
-
-/**
- * {@link State} implementation that signals failure.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class FailedState extends AbstractState {
-
-       /** A failed state instance without an exception. */
-       public static final State INSTANCE = new FailedState();
-
-       /**
-        * Creates a new failed state.
-        */
-       public FailedState() {
-               super(false);
-       }
-
-       /**
-        * Creates a new failed state with the given exception
-        *
-        * @param exception
-        *            The exception of the state
-        */
-       public FailedState(Throwable exception) {
-               super(exception);
-       }
-
-       //
-       // STATIC METHODS
-       //
-
-       /**
-        * Returns a failed state for the given state. The failed state will be
-        * unsuccessful ({@link #success()} returns false) and it will contain the
-        * same {@link #exception()} as the given state.
-        *
-        * @param state
-        *            The state to copy the exception from
-        * @return A failed state
-        */
-       public static FailedState from(State state) {
-               if (state instanceof FailedState) {
-                       return (FailedState) state;
-               }
-               return new FailedState(state.exception());
-       }
-
-       //
-       // OBJECT METHODS
-       //
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       public String toString() {
-               return String.format("%s[exception=%s]", getClass().getSimpleName(), exception());
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/reactor/states/FileState.java b/src/main/java/net/pterodactylus/reactor/states/FileState.java
deleted file mode 100644 (file)
index b3bb70a..0000000
+++ /dev/null
@@ -1,129 +0,0 @@
-/*
- * Reactor - FileState.java - Copyright © 2013 David Roden
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.reactor.states;
-
-import net.pterodactylus.reactor.State;
-
-/**
- * A {@link State} that contains information about a file.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class FileState extends AbstractState {
-
-       /** Whether the file exists. */
-       private final boolean exists;
-
-       /** Whether the file is readable. */
-       private final boolean readable;
-
-       /** The size of the file. */
-       private final long size;
-
-       /** The modification time of the file. */
-       private final long modificationTime;
-
-       /**
-        * Creates a new file state that signals that an exceptio occured during
-        * retrieval.
-        *
-        * @param exception
-        *            The exception that occured
-        */
-       public FileState(Throwable exception) {
-               super(exception);
-               exists = false;
-               readable = false;
-               size = -1;
-               modificationTime = -1;
-       }
-
-       /**
-        * Creates a new file state.
-        *
-        * @param exists
-        *            {@code true} if the file exists, {@code false} otherwise
-        * @param readable
-        *            {@code true} if the file is readable, {@code false} otherwise
-        * @param size
-        *            The size of the file (in bytes)
-        * @param modificationTime
-        *            The modification time of the file (in milliseconds since Jan
-        *            1, 1970 UTC)
-        */
-       public FileState(boolean exists, boolean readable, long size, long modificationTime) {
-               this.exists = exists;
-               this.readable = readable;
-               this.size = size;
-               this.modificationTime = modificationTime;
-       }
-
-       //
-       // ACCESSORS
-       //
-
-       /**
-        * Returns whether the file exists.
-        *
-        * @return {@code true} if the file exists, {@code false} otherwise
-        */
-       public boolean exists() {
-               return exists;
-       }
-
-       /**
-        * Returns whether the file is readable.
-        *
-        * @return {@code true} if the file is readable, {@code false} otherwise
-        */
-       public boolean readable() {
-               return readable;
-       }
-
-       /**
-        * Returns the size of the file.
-        *
-        * @return The size of the file (in bytes)
-        */
-       public long size() {
-               return size;
-       }
-
-       /**
-        * Returns the modification time of the file.
-        *
-        * @return The modification time of the file (in milliseconds since Jan 1,
-        *         1970 UTC)
-        */
-       public long modificationTime() {
-               return modificationTime;
-       }
-
-       //
-       // OBJECT METHODS
-       //
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       public String toString() {
-               return String.format("%s[exists=%s,readable=%s,size=%s,modificationTime=%d(%5$tc)", getClass().getSimpleName(), exists(), readable(), size(), modificationTime());
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/reactor/states/HtmlState.java b/src/main/java/net/pterodactylus/reactor/states/HtmlState.java
deleted file mode 100644 (file)
index ce89a98..0000000
+++ /dev/null
@@ -1,84 +0,0 @@
-/*
- * Reactor - HtmlState.java - Copyright © 2013 David Roden
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.reactor.states;
-
-import net.pterodactylus.reactor.State;
-
-import org.jsoup.nodes.Document;
-
-/**
- * {@link State} implementation that contains a parsed HTML {@link Document}.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class HtmlState extends AbstractState {
-
-       /** The URI of the parsed document. */
-       private final String uri;
-
-       /** The parsed document. */
-       private final Document document;
-
-       /**
-        * Creates a new HTML state.
-        *
-        * @param uri
-        *            The URI of the parsed document
-        * @param document
-        *            The parsed documnet
-        */
-       public HtmlState(String uri, Document document) {
-               this.uri = uri;
-               this.document = document;
-       }
-
-       //
-       // ACCESSORS
-       //
-
-       /**
-        * Returns the URI of the parsed document.
-        *
-        * @return The URI of the parsed document
-        */
-       public String uri() {
-               return uri;
-       }
-
-       /**
-        * Returns the parsed document.
-        *
-        * @return The parsed document
-        */
-       public Document document() {
-               return document;
-       }
-
-       //
-       // OBJECT METHODS
-       //
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       public String toString() {
-               return String.format("%s[document=(%s chars)]", getClass().getSimpleName(), document().toString().length());
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/reactor/states/HttpState.java b/src/main/java/net/pterodactylus/reactor/states/HttpState.java
deleted file mode 100644 (file)
index 442106b..0000000
+++ /dev/null
@@ -1,154 +0,0 @@
-/*
- * Reactor - HttpState.java - Copyright © 2013 David Roden
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.reactor.states;
-
-import java.io.UnsupportedEncodingException;
-
-import net.pterodactylus.reactor.State;
-import net.pterodactylus.reactor.queries.HttpQuery;
-
-import org.apache.http.HeaderElement;
-import org.apache.http.NameValuePair;
-import org.apache.http.message.BasicHeaderValueParser;
-
-/**
- * {@link State} that contains the results of an {@link HttpQuery}.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class HttpState extends AbstractState {
-
-       /** The URI that was requested. */
-       private final String uri;
-
-       /** The protocol code. */
-       private final int protocolCode;
-
-       /** The content type. */
-       private final String contentType;
-
-       /** The result. */
-       private final byte[] rawResult;
-
-       /**
-        * Creates a new HTTP state.
-        *
-        * @param uri
-        *            The URI that was requested
-        * @param protocolCode
-        *            The code of the reply
-        * @param contentType
-        *            The content type of the reply
-        * @param rawResult
-        *            The raw result
-        */
-       public HttpState(String uri, int protocolCode, String contentType, byte[] rawResult) {
-               this.uri = uri;
-               this.protocolCode = protocolCode;
-               this.contentType = contentType;
-               this.rawResult = rawResult;
-       }
-
-       //
-       // ACCESSORS
-       //
-
-       /**
-        * Returns the URI that was requested.
-        *
-        * @return The URI that was request
-        */
-       public String uri() {
-               return uri;
-       }
-
-       /**
-        * Returns the protocol code of the reply.
-        *
-        * @return The protocol code of the reply
-        */
-       public int protocolCode() {
-               return protocolCode;
-       }
-
-       /**
-        * Returns the content type of the reply.
-        *
-        * @return The content type of the reply
-        */
-       public String contentType() {
-               return contentType;
-       }
-
-       /**
-        * Returns the raw result of the reply.
-        *
-        * @return The raw result of the reply
-        */
-       public byte[] rawResult() {
-               return rawResult;
-       }
-
-       /**
-        * Returns the decoded content of the reply. This method uses the charset
-        * information from the {@link #contentType()}, if present, or UTF-8 if no
-        * content type is present.
-        *
-        * @return The decoded content
-        */
-       public String content() {
-               try {
-                       return new String(rawResult(), extractCharset(contentType()));
-               } catch (UnsupportedEncodingException uee1) {
-                       throw new RuntimeException(String.format("Could not decode content as %s.", extractCharset(contentType())), uee1);
-               }
-       }
-
-       //
-       // STATIC METHODS
-       //
-
-       /**
-        * Extracts charset information from the given content type.
-        *
-        * @param contentType
-        *            The content type response header
-        * @return The extracted charset, or UTF-8 if no charset could be extracted
-        */
-       private static String extractCharset(String contentType) {
-               if (contentType == null) {
-                       return "ISO-8859-1";
-               }
-               HeaderElement headerElement = BasicHeaderValueParser.parseHeaderElement(contentType, new BasicHeaderValueParser());
-               NameValuePair charset = headerElement.getParameterByName("charset");
-               return (charset != null) ? charset.getValue() : "ISO-8859-1";
-       }
-
-       //
-       // OBJECT METHODS
-       //
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       public String toString() {
-               return String.format("%s[uri=%s,protocolCode=%d,contentType=%s,rawResult=(%s bytes)]", getClass().getSimpleName(), uri(), protocolCode(), contentType(), rawResult().length);
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/reactor/states/StateManager.java b/src/main/java/net/pterodactylus/reactor/states/StateManager.java
deleted file mode 100644 (file)
index 8046153..0000000
+++ /dev/null
@@ -1,154 +0,0 @@
-/*
- * Reactor - StateManager.java - Copyright © 2013 David Roden
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.reactor.states;
-
-import java.io.File;
-import java.io.IOException;
-
-import net.pterodactylus.reactor.State;
-
-import org.apache.log4j.Logger;
-
-import com.fasterxml.jackson.core.JsonGenerationException;
-import com.fasterxml.jackson.core.JsonParseException;
-import com.fasterxml.jackson.databind.JsonMappingException;
-import com.fasterxml.jackson.databind.ObjectMapper;
-
-/**
- * Loads and saves {@link State}s.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class StateManager {
-
-       /** The logger. */
-       private static final Logger logger = Logger.getLogger(StateManager.class);
-
-       /** Jackson object mapper. */
-       private final ObjectMapper objectMapper = new ObjectMapper();
-
-       /** The directory in which to store states. */
-       private final String directory;
-
-       /**
-        * Creates a new state manager. The given directory is assumed to exist.
-        *
-        * @param directory
-        *            The directory to store states in
-        */
-       public StateManager(String directory) {
-               this.directory = directory;
-       }
-
-       //
-       // ACTIONS
-       //
-
-       /**
-        * Loads the last state with the given name.
-        *
-        * @param reactionName
-        *            The name of the reaction
-        * @return The loaded state, or {@code null} if the state could not be
-        *         loaded
-        */
-       public State loadLastState(String reactionName) {
-               return loadLastState(reactionName, false);
-       }
-
-       /**
-        * Loads the last state with the given name.
-        *
-        * @param reactionName
-        *            The name of the reaction
-        * @return The loaded state, or {@code null} if the state could not be
-        *         loaded
-        */
-       public State loadLastSuccessfulState(String reactionName) {
-               return loadLastState(reactionName, true);
-       }
-
-       /**
-        * Saves the given state under the given name.
-        *
-        * @param reactionName
-        *            The name of the reaction
-        * @param state
-        *            The state to save
-        */
-       public void saveState(String reactionName, State state) {
-               try {
-                       File stateFile = stateFile(reactionName, "last");
-                       objectMapper.writeValue(stateFile, state);
-                       if (state.success()) {
-                               stateFile = stateFile(reactionName, "success");
-                               objectMapper.writeValue(stateFile, state);
-                       }
-               } catch (JsonGenerationException jge1) {
-                       logger.warn(String.format("State for Reaction “%s” could not be generated.", reactionName), jge1);
-               } catch (JsonMappingException jme1) {
-                       logger.warn(String.format("State for Reaction “%s” could not be generated.", reactionName), jme1);
-               } catch (IOException ioe1) {
-                       logger.warn(String.format("State for Reaction “%s” could not be written.", reactionName));
-               }
-       }
-
-       //
-       // PRIVATE METHODS
-       //
-
-       /**
-        * Returns the file for the state with the given name.
-        *
-        * @param reactionName
-        *            The name of the reaction
-        * @param suffix
-        *            An additional suffix (may be {@code null}
-        * @return The file for the state
-        */
-       private File stateFile(String reactionName, String suffix) {
-               return new File(directory, reactionName + ((suffix != null) ? "." + suffix : "") + ".json");
-       }
-
-       /**
-        * Load the given state for the reaction with the given name.
-        *
-        * @param reactionName
-        *            The name of the reaction
-        * @param successful
-        *            {@code true} to load the last successful state, {@code false}
-        *            to load the last state
-        * @return The loaded state, or {@code null} if the state could not be
-        *         loaded
-        */
-       private State loadLastState(String reactionName, boolean successful) {
-               File stateFile = stateFile(reactionName, successful ? "success" : "last");
-               try {
-                       State state = objectMapper.readValue(stateFile, AbstractState.class);
-                       return state;
-               } catch (JsonParseException jpe1) {
-                       logger.warn(String.format("State for Reaction “%s” could not be parsed.", reactionName), jpe1);
-               } catch (JsonMappingException jme1) {
-                       logger.warn(String.format("State for Reaction “%s” could not be parsed.", reactionName), jme1);
-               } catch (IOException ioe1) {
-                       logger.info(String.format("State for Reaction “%s” could not be found.", reactionName));
-               }
-               return null;
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/reactor/states/TorrentState.java b/src/main/java/net/pterodactylus/reactor/states/TorrentState.java
deleted file mode 100644 (file)
index f3b3546..0000000
+++ /dev/null
@@ -1,305 +0,0 @@
-/*
- * Reactor - TorrentState.java - Copyright © 2013 David Roden
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.reactor.states;
-
-import java.nio.charset.Charset;
-import java.util.Iterator;
-import java.util.List;
-
-import net.pterodactylus.reactor.State;
-import net.pterodactylus.reactor.states.TorrentState.TorrentFile;
-
-import org.apache.http.NameValuePair;
-import org.apache.http.client.utils.URLEncodedUtils;
-
-import com.fasterxml.jackson.annotation.JsonProperty;
-import com.google.common.collect.Lists;
-
-/**
- * {@link State} that contains information about an arbitrary number of torrent
- * files.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class TorrentState extends AbstractState implements Iterable<TorrentFile> {
-
-       /** The torrent files. */
-       @JsonProperty
-       private List<TorrentFile> files = Lists.newArrayList();
-
-       //
-       // ACCESSORS
-       //
-
-       /**
-        * Adds a torrent file to this state.
-        *
-        * @param torrentFile
-        *            The torrent file to add
-        * @return This state
-        */
-       public TorrentState addTorrentFile(TorrentFile torrentFile) {
-               files.add(torrentFile);
-               return this;
-       }
-
-       //
-       // ITERABLE METHODS
-       //
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       public Iterator<TorrentFile> iterator() {
-               return files.iterator();
-       }
-
-       //
-       // OBJECT METHODS
-       //
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       public String toString() {
-               return String.format("%s[files=%s]", getClass().getSimpleName(), files);
-       }
-
-       /**
-        * Container for torrent file data.
-        *
-        * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
-        */
-       public static class TorrentFile {
-
-               /** The name of the file. */
-               @JsonProperty
-               private final String name;
-
-               /** The size of the file. */
-               @JsonProperty
-               private final String size;
-
-               /** The magnet URI of the file. */
-               @JsonProperty
-               private final String magnetUri;
-
-               /** The download URI of the file. */
-               @JsonProperty
-               private final String downloadUri;
-
-               /** The number of files in this torrent. */
-               @JsonProperty
-               private final int fileCount;
-
-               /** The number of seeds connected to this torrent. */
-               @JsonProperty
-               private final int seedCount;
-
-               /** The number of leechers connected to this torrent. */
-               @JsonProperty
-               private final int leechCount;
-
-               /**
-                * No-arg constructor for deserialization.
-                */
-               @SuppressWarnings("unused")
-               private TorrentFile() {
-                       this(null, null, null, null, 0, 0, 0);
-               }
-
-               /**
-                * Creates a new torrent file.
-                *
-                * @param name
-                *            The name of the file
-                * @param size
-                *            The size of the file
-                * @param magnetUri
-                *            The magnet URI of the file
-                * @param downloadUri
-                *            The download URI of the file
-                * @param fileCount
-                *            The number of files
-                * @param seedCount
-                *            The number of connected seeds
-                * @param leechCount
-                *            The number of connected leechers
-                */
-               public TorrentFile(String name, String size, String magnetUri, String downloadUri, int fileCount, int seedCount, int leechCount) {
-                       this.name = name;
-                       this.size = size;
-                       this.magnetUri = magnetUri;
-                       this.downloadUri = downloadUri;
-                       this.fileCount = fileCount;
-                       this.seedCount = seedCount;
-                       this.leechCount = leechCount;
-               }
-
-               //
-               // ACCESSORS
-               //
-
-               /**
-                * Returns the name of the file.
-                *
-                * @return The name of the file
-                */
-               public String name() {
-                       return name;
-               }
-
-               /**
-                * Returns the size of the file. The returned size may included
-                * non-numeric information, such as units (e. g. “860.46 MB”).
-                *
-                * @return The size of the file
-                */
-               public String size() {
-                       return size;
-               }
-
-               /**
-                * Returns the magnet URI of the file.
-                *
-                * @return The magnet URI of the file
-                */
-               public String magnetUri() {
-                       return magnetUri;
-               }
-
-               /**
-                * Returns the download URI of the file.
-                *
-                * @return The download URI of the file
-                */
-               public String downloadUri() {
-                       return downloadUri;
-               }
-
-               /**
-                * Returns the number of files in this torrent.
-                *
-                * @return The number of files in this torrent
-                */
-               public int fileCount() {
-                       return fileCount;
-               }
-
-               /**
-                * Returns the number of seeds connected to this torrent.
-                *
-                * @return The number of connected seeds
-                */
-               public int seedCount() {
-                       return seedCount;
-               }
-
-               /**
-                * Returns the number of leechers connected to this torrent.
-                *
-                * @return The number of connected leechers
-                */
-               public int leechCount() {
-                       return leechCount;
-               }
-
-               //
-               // PRIVATE METHODS
-               //
-
-               /**
-                * Generates an ID for this file. If a {@link #magnetUri} is set, an ID
-                * is {@link #extractId(String) extracted} from it. Otherwise the magnet
-                * URI is used. If the {@link #magnetUri} is not set, the
-                * {@link #downloadUri} is used. If that is not set either, the name of
-                * the file is returned.
-                *
-                * @return The generated ID
-                */
-               private String generateId() {
-                       if (magnetUri != null) {
-                               String id = extractId(magnetUri);
-                               if (id != null) {
-                                       return id;
-                               }
-                               return magnetUri;
-                       }
-                       return (downloadUri != null) ? downloadUri : name;
-               }
-
-               //
-               // STATIC METHODS
-               //
-
-               /**
-                * Tries to extract the “exact target” of a magnet URI.
-                *
-                * @param magnetUri
-                *            The magnet URI to extract the “xt” from
-                * @return The extracted ID, or {@code null} if no ID could be found
-                */
-               private static String extractId(String magnetUri) {
-                       List<NameValuePair> parameters = URLEncodedUtils.parse(magnetUri.substring("magnet:?".length()), Charset.forName("UTF-8"));
-                       for (NameValuePair parameter : parameters) {
-                               if (parameter.getName().equals("xt")) {
-                                       return parameter.getValue();
-                               }
-                       }
-                       return null;
-               }
-
-               //
-               // OBJECT METHODS
-               //
-
-               /**
-                * {@inheritDoc}
-                */
-               @Override
-               public int hashCode() {
-                       return (generateId() != null) ? generateId().hashCode() : 0;
-               }
-
-               /**
-                * {@inheritDoc}
-                */
-               @Override
-               public boolean equals(Object object) {
-                       if (!(object instanceof TorrentFile)) {
-                               return false;
-                       }
-                       if (generateId() != null) {
-                               return generateId().equals(((TorrentFile) object).generateId());
-                       }
-                       return false;
-               }
-
-               /**
-                * {@inheritDoc}
-                */
-               @Override
-               public String toString() {
-                       return String.format("%s(%s,%s,%s)", name(), size(), magnetUri(), downloadUri());
-               }
-
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/reactor/triggers/AlwaysTrigger.java b/src/main/java/net/pterodactylus/reactor/triggers/AlwaysTrigger.java
deleted file mode 100644 (file)
index 5971553..0000000
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * Reactor - AlwaysTrigger.java - Copyright © 2013 David Roden
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.reactor.triggers;
-
-import net.pterodactylus.reactor.Reaction;
-import net.pterodactylus.reactor.State;
-import net.pterodactylus.reactor.Trigger;
-import net.pterodactylus.reactor.output.DefaultOutput;
-import net.pterodactylus.reactor.output.Output;
-
-/**
- * {@link Trigger} implementation that always triggers.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class AlwaysTrigger implements Trigger {
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       public boolean triggers(State currentState, State previousState) {
-               return true;
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       public Output output(Reaction reaction) {
-               return new DefaultOutput("true").addText("text/plain", "true").addText("text/html", "<div>true</div>");
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/reactor/triggers/FileExistenceTrigger.java b/src/main/java/net/pterodactylus/reactor/triggers/FileExistenceTrigger.java
deleted file mode 100644 (file)
index a285843..0000000
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- * Reactor - FileExistenceTrigger.java - Copyright © 2013 David Roden
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.reactor.triggers;
-
-import net.pterodactylus.reactor.Reaction;
-import net.pterodactylus.reactor.State;
-import net.pterodactylus.reactor.Trigger;
-import net.pterodactylus.reactor.output.DefaultOutput;
-import net.pterodactylus.reactor.output.Output;
-import net.pterodactylus.reactor.states.FileState;
-
-import com.google.common.base.Preconditions;
-
-/**
- * A trigger that detects changes in the existence of a file.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class FileExistenceTrigger implements Trigger {
-
-       //
-       // TRIGGER METHODS
-       //
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       public boolean triggers(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();
-       }
-
-       /**
-        * {@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>");
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/reactor/triggers/FileStateModifiedTrigger.java b/src/main/java/net/pterodactylus/reactor/triggers/FileStateModifiedTrigger.java
deleted file mode 100644 (file)
index 39044c3..0000000
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- * Reactor - FileStateModifiedTrigger.java - Copyright © 2013 David Roden
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.reactor.triggers;
-
-import static com.google.common.base.Preconditions.checkState;
-import net.pterodactylus.reactor.Reaction;
-import net.pterodactylus.reactor.State;
-import net.pterodactylus.reactor.Trigger;
-import net.pterodactylus.reactor.output.DefaultOutput;
-import net.pterodactylus.reactor.output.Output;
-import net.pterodactylus.reactor.states.FileState;
-
-/**
- * {@link Trigger} that checks for modifications of a file using the existence,
- * size, and modification time of the {@link FileState}.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class FileStateModifiedTrigger implements Trigger {
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       public boolean triggers(State currentState, State previousState) {
-               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());
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       public Output output(Reaction reaction) {
-               return new DefaultOutput("File modified").addText("text/plain", "File modified").addText("text/html", "<div>File modified</div>");
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/reactor/triggers/NewEpisodeTrigger.java b/src/main/java/net/pterodactylus/reactor/triggers/NewEpisodeTrigger.java
deleted file mode 100644 (file)
index 4504d60..0000000
+++ /dev/null
@@ -1,233 +0,0 @@
-/*
- * Reactor - NewEpisodeTrigger.java - Copyright © 2013 David Roden
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.reactor.triggers;
-
-import static com.google.common.base.Preconditions.checkState;
-
-import java.util.Collection;
-
-import net.pterodactylus.reactor.Reaction;
-import net.pterodactylus.reactor.State;
-import net.pterodactylus.reactor.Trigger;
-import net.pterodactylus.reactor.output.DefaultOutput;
-import net.pterodactylus.reactor.output.Output;
-import net.pterodactylus.reactor.states.EpisodeState;
-import net.pterodactylus.reactor.states.EpisodeState.Episode;
-import net.pterodactylus.reactor.states.TorrentState.TorrentFile;
-
-import org.apache.commons.lang3.StringEscapeUtils;
-
-import com.google.common.base.Predicate;
-import com.google.common.collect.Collections2;
-
-/**
- * {@link Trigger} implementation that compares two {@link EpisodeState}s for
- * new and changed {@link Episode}s.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class NewEpisodeTrigger implements Trigger {
-
-       /** All new episodes. */
-       private Collection<Episode> newEpisodes;
-
-       /** All changed episodes. */
-       private Collection<Episode> changedEpisodes;
-
-       //
-       // TRIGGER METHODS
-       //
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       public boolean triggers(State currentState, State previousState) {
-               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<Episode>() {
-
-                       @Override
-                       public boolean apply(Episode episode) {
-                               return !previousEpisodeState.episodes().contains(episode);
-                       }
-               });
-
-               changedEpisodes = Collections2.filter(currentEpisodeState.episodes(), new Predicate<Episode>() {
-
-                       @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<TorrentFile> newTorrentFiles = Collections2.filter(episode.torrentFiles(), new Predicate<TorrentFile>() {
-
-                                       @Override
-                                       public boolean apply(TorrentFile torrentFile) {
-                                               return !previousEpisode.torrentFiles().contains(torrentFile);
-                                       }
-                               });
-
-                               return !newTorrentFiles.isEmpty();
-                       }
-
-                       private Episode findPreviousEpisode(Episode episode) {
-                               for (Episode previousStateEpisode : previousEpisodeState) {
-                                       if (previousStateEpisode.equals(episode)) {
-                                               return previousStateEpisode;
-                                       }
-                               }
-                               return null;
-                       }
-
-               });
-
-               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, newEpisodes, changedEpisodes));
-               output.addText("text/html", generateHtmlText(reaction, newEpisodes, changedEpisodes));
-               return output;
-       }
-
-       //
-       // STATIC METHODS
-       //
-
-       /**
-        * Generates the plain text trigger output.
-        *
-        * @param reaction
-        *            The reaction that was triggered
-        * @param newEpisodes
-        *            The new episodes
-        * @param changedEpisodes
-        *            The changed episodes
-        * @return The plain text output
-        */
-       private static String generatePlainText(Reaction reaction, Collection<Episode> newEpisodes, Collection<Episode> changedEpisodes) {
-               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");
-                                       stringBuilder.append("    Magnet: ").append(torrentFile.magnetUri()).append("\n");
-                                       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");
-                                       stringBuilder.append("    Magnet: ").append(torrentFile.magnetUri()).append("\n");
-                                       stringBuilder.append("    Download: ").append(torrentFile.downloadUri()).append("\n");
-                               }
-                       }
-               }
-               return stringBuilder.toString();
-       }
-
-       /**
-        * Generates the HTML trigger output.
-        *
-        * @param reaction
-        *            The reaction that was triggered
-        * @param newEpisodes
-        *            The new episodes
-        * @param changedEpisodes
-        *            The changed episodes
-        * @return The HTML output
-        */
-       private static String generateHtmlText(Reaction reaction, Collection<Episode> newEpisodes, Collection<Episode> changedEpisodes) {
-               StringBuilder htmlBuilder = new StringBuilder();
-               htmlBuilder.append("<html><body>\n");
-               htmlBuilder.append("<h1>").append(StringEscapeUtils.escapeHtml4(reaction.name())).append("</h1>\n");
-               if (!newEpisodes.isEmpty()) {
-                       htmlBuilder.append("<h2>New Episodes</h2>\n");
-                       htmlBuilder.append("<ul>\n");
-                       for (Episode episode : newEpisodes) {
-                               htmlBuilder.append("<li>Season ").append(episode.season()).append(", Episode ").append(episode.episode()).append("</li>\n");
-                               htmlBuilder.append("<ul>\n");
-                               for (TorrentFile torrentFile : episode) {
-                                       htmlBuilder.append("<li>").append(StringEscapeUtils.escapeHtml4(torrentFile.name())).append("</li>\n");
-                                       htmlBuilder.append("<div>");
-                                       htmlBuilder.append("<strong>").append(StringEscapeUtils.escapeHtml4(torrentFile.size())).append("</strong>, ");
-                                       htmlBuilder.append("<strong>").append(torrentFile.fileCount()).append("</strong> file(s), ");
-                                       htmlBuilder.append("<strong>").append(torrentFile.seedCount()).append("</strong> seed(s), ");
-                                       htmlBuilder.append("<strong>").append(torrentFile.leechCount()).append("</strong> leecher(s)</div>\n");
-                                       htmlBuilder.append("<div><a href=\"").append(StringEscapeUtils.escapeHtml4(torrentFile.magnetUri())).append("\">Magnet</a> ");
-                                       htmlBuilder.append("<a href=\"").append(StringEscapeUtils.escapeHtml4(torrentFile.downloadUri())).append("\">Download</a></div>\n");
-                               }
-                               htmlBuilder.append("</ul>\n");
-                       }
-                       htmlBuilder.append("</ul>\n");
-               }
-               if (!changedEpisodes.isEmpty()) {
-                       htmlBuilder.append("<h2>Changed Episodes</h2>\n");
-                       htmlBuilder.append("<ul>\n");
-                       for (Episode episode : changedEpisodes) {
-                               htmlBuilder.append("<li>Season ").append(episode.season()).append(", Episode ").append(episode.episode()).append("</li>\n");
-                               htmlBuilder.append("<ul>\n");
-                               for (TorrentFile torrentFile : episode) {
-                                       htmlBuilder.append("<li>").append(StringEscapeUtils.escapeHtml4(torrentFile.name())).append("</li>\n");
-                                       htmlBuilder.append("<div>");
-                                       htmlBuilder.append("<strong>").append(StringEscapeUtils.escapeHtml4(torrentFile.size())).append("</strong>, ");
-                                       htmlBuilder.append("<strong>").append(torrentFile.fileCount()).append("</strong> file(s), ");
-                                       htmlBuilder.append("<strong>").append(torrentFile.seedCount()).append("</strong> seed(s), ");
-                                       htmlBuilder.append("<strong>").append(torrentFile.leechCount()).append("</strong> leecher(s)</div>\n");
-                                       htmlBuilder.append("<div><a href=\"").append(StringEscapeUtils.escapeHtml4(torrentFile.magnetUri())).append("\">Magnet</a> ");
-                                       htmlBuilder.append("<a href=\"").append(StringEscapeUtils.escapeHtml4(torrentFile.downloadUri())).append("\">Download</a></div>\n");
-                               }
-                               htmlBuilder.append("</ul>\n");
-                       }
-                       htmlBuilder.append("</ul>\n");
-               }
-               htmlBuilder.append("</body></html>\n");
-               return htmlBuilder.toString();
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/reactor/triggers/NewTorrentTrigger.java b/src/main/java/net/pterodactylus/reactor/triggers/NewTorrentTrigger.java
deleted file mode 100644 (file)
index ab640c6..0000000
+++ /dev/null
@@ -1,130 +0,0 @@
-/*
- * Reactor - NewTorrentTrigger.java - Copyright © 2013 David Roden
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.reactor.triggers;
-
-import static com.google.common.base.Preconditions.checkState;
-
-import java.util.List;
-
-import net.pterodactylus.reactor.Reaction;
-import net.pterodactylus.reactor.State;
-import net.pterodactylus.reactor.Trigger;
-import net.pterodactylus.reactor.output.DefaultOutput;
-import net.pterodactylus.reactor.output.Output;
-import net.pterodactylus.reactor.states.TorrentState;
-import net.pterodactylus.reactor.states.TorrentState.TorrentFile;
-
-import org.apache.commons.lang3.StringEscapeUtils;
-
-import com.google.common.collect.Lists;
-
-/**
- * {@link Trigger} implementation that is triggered by {@link TorrentFile}s that
- * appear in the current {@link TorrentState} but not in the previous one.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class NewTorrentTrigger implements Trigger {
-
-       /** The newly detected torrent files. */
-       private List<TorrentFile> torrentFiles = Lists.newArrayList();
-
-       //
-       // TRIGGER METHODS
-       //
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       public boolean triggers(State currentState, State previousState) {
-               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();
-               for (TorrentFile torrentFile : currentTorrentState) {
-                       torrentFiles.add(torrentFile);
-               }
-               for (TorrentFile torrentFile : previousTorrentState) {
-                       torrentFiles.remove(torrentFile);
-               }
-               return !torrentFiles.isEmpty();
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       @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));
-               return output;
-       }
-
-       //
-       // STATIC METHODS
-       //
-
-       /**
-        * Generates a plain text list of torrent files.
-        *
-        * @param torrentFiles
-        *            The torrent files to list
-        * @return The generated plain text
-        */
-       private static String getPlainTextList(List<TorrentFile> torrentFiles) {
-               StringBuilder plainText = new StringBuilder();
-               plainText.append("New Torrents:\n\n");
-               for (TorrentFile torrentFile : torrentFiles) {
-                       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");
-                       plainText.append('\t').append(torrentFile.magnetUri()).append('\n');
-                       plainText.append('\t').append(torrentFile.downloadUri()).append('\n');
-                       plainText.append('\n');
-               }
-               return plainText.toString();
-       }
-
-       /**
-        * Generates an HTML list of the given torrent files.
-        *
-        * @param torrentFiles
-        *            The torrent files to list
-        * @return The generated HTML
-        */
-       private static String getHtmlTextList(List<TorrentFile> torrentFiles) {
-               StringBuilder htmlText = new StringBuilder();
-               htmlText.append("<html><body>\n");
-               htmlText.append("<h1>New Torrents</h1>\n");
-               htmlText.append("<ul>\n");
-               for (TorrentFile torrentFile : torrentFiles) {
-                       htmlText.append("<li><strong>").append(StringEscapeUtils.escapeHtml4(torrentFile.name())).append("</strong></li>");
-                       htmlText.append("<div>Size: <strong>").append(StringEscapeUtils.escapeHtml4(torrentFile.size())).append("</strong> in <strong>").append(torrentFile.fileCount()).append("</strong> file(s)</div>");
-                       htmlText.append("<div><strong>").append(torrentFile.seedCount()).append("</strong> seed(s), <strong>").append(torrentFile.leechCount()).append("</strong> leecher(s)</div>");
-                       htmlText.append(String.format("<div><a href=\"%s\">Magnet URI</a></div>", StringEscapeUtils.escapeHtml4(torrentFile.magnetUri())));
-                       htmlText.append(String.format("<div><a href=\"%s\">Download URI</a></div>", StringEscapeUtils.escapeHtml4(torrentFile.downloadUri())));
-               }
-               htmlText.append("</ul>\n");
-               htmlText.append("</body></html>\n");
-               return htmlText.toString();
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/rhynodge/Action.java b/src/main/java/net/pterodactylus/rhynodge/Action.java
new file mode 100644 (file)
index 0000000..4a7ed56
--- /dev/null
@@ -0,0 +1,38 @@
+/*
+ * Rhynodge - Action.java - Copyright © 2013 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.rhynodge;
+
+import net.pterodactylus.rhynodge.output.Output;
+
+/**
+ * An action is performed when a {@link Trigger} determines that two given
+ * {@link State}s of a {@link Query} signify a change.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public interface Action {
+
+       /**
+        * Performs the action.
+        *
+        * @param output
+        *            The output for the action
+        */
+       void execute(Output output);
+
+}
diff --git a/src/main/java/net/pterodactylus/rhynodge/Filter.java b/src/main/java/net/pterodactylus/rhynodge/Filter.java
new file mode 100644 (file)
index 0000000..b175b48
--- /dev/null
@@ -0,0 +1,43 @@
+/*
+ * Rhynodge - Filter.java - Copyright © 2013 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.rhynodge;
+
+/**
+ * Defines a filter that runs between {@link Query}s and {@link Trigger}s and
+ * can be used to convert a {@link State} into another {@link State}. This can
+ * be used to extract further information from a state.
+ * <p>
+ * An example scenario would be a {@link Query} that requests a web site and a
+ * {@link Filter} that extracts content from the web site. That way the same
+ * {@link Query} could be used for multiple {@link Reaction}s without requiring
+ * modifications.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public interface Filter {
+
+       /**
+        * Converts the given state into a different state.
+        *
+        * @param state
+        *            The state to convert
+        * @return The new state
+        */
+       State filter(State state);
+
+}
diff --git a/src/main/java/net/pterodactylus/rhynodge/Query.java b/src/main/java/net/pterodactylus/rhynodge/Query.java
new file mode 100644 (file)
index 0000000..82ce8b6
--- /dev/null
@@ -0,0 +1,35 @@
+/*
+ * Rhynodge - Query.java - Copyright © 2013 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.rhynodge;
+
+/**
+ * A query is used to retrieve the current {@link State} of a system.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public interface Query {
+
+       /**
+        * Retrieves the current state of the system. The returned state is never
+        * {@code null}.
+        *
+        * @return The current state of the system.
+        */
+       public State state();
+
+}
diff --git a/src/main/java/net/pterodactylus/rhynodge/Reaction.java b/src/main/java/net/pterodactylus/rhynodge/Reaction.java
new file mode 100644 (file)
index 0000000..69e2592
--- /dev/null
@@ -0,0 +1,160 @@
+/*
+ * Rhynodge - Reaction.java - Copyright © 2013 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.rhynodge;
+
+import java.util.Collections;
+import java.util.List;
+
+import com.google.common.collect.Lists;
+
+/**
+ * A {@code Reaction} binds together {@link Query}s, {@link Trigger}s, and
+ * {@link Action}s, and it stores the intermediary {@link State}s.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class Reaction {
+
+       /** The name of this reaction. */
+       private final String name;
+
+       /** The query to run. */
+       private final Query query;
+
+       /** The filters to run. */
+       private final List<Filter> filters = Lists.newArrayList();
+
+       /** The trigger to detect changes. */
+       private final Trigger trigger;
+
+       /** The action to perform. */
+       private final Action action;
+
+       /** The interval in which to run queries (in milliseconds). */
+       private long updateInterval;
+
+       /**
+        * Creates a new reaction.
+        *
+        * @param name
+        *            The name of the reaction
+        * @param query
+        *            The query to run
+        * @param trigger
+        *            The trigger to detect changes
+        * @param action
+        *            The action to perform
+        */
+       public Reaction(String name, Query query, Trigger trigger, Action action) {
+               this(name, query, Collections.<Filter> emptyList(), trigger, action);
+       }
+
+       /**
+        * Creates a new reaction.
+        *
+        * @param name
+        *            The name of the reaction
+        * @param query
+        *            The query to run
+        * @param filters
+        *            The filters to run
+        * @param trigger
+        *            The trigger to detect changes
+        * @param action
+        *            The action to perform
+        */
+       public Reaction(String name, Query query, List<Filter> filters, Trigger trigger, Action action) {
+               this.name = name;
+               this.query = query;
+               this.filters.addAll(filters);
+               this.trigger = trigger;
+               this.action = action;
+       }
+
+       //
+       // ACCESSORS
+       //
+
+       /**
+        * Returns the name of this reaction. This name is solely used for display
+        * purposes and does not need to be unique.
+        *
+        * @return The name of this reaction
+        */
+       public String name() {
+               return name;
+       }
+
+       /**
+        * Returns the query to run.
+        *
+        * @return The query to run
+        */
+       public Query query() {
+               return query;
+       }
+
+       /**
+        * Returns the filters to run.
+        *
+        * @return The filters to run
+        */
+       public Iterable<Filter> filters() {
+               return filters;
+       }
+
+       /**
+        * Returns the trigger to detect changes.
+        *
+        * @return The trigger to detect changes
+        */
+       public Trigger trigger() {
+               return trigger;
+       }
+
+       /**
+        * Returns the action to perform.
+        *
+        * @return The action to perform
+        */
+       public Action action() {
+               return action;
+       }
+
+       /**
+        * Returns the update interval of this reaction.
+        *
+        * @return The update interval of this reaction (in milliseconds)
+        */
+       public long updateInterval() {
+               return updateInterval;
+       }
+
+       /**
+        * Sets the update interval of this reaction.
+        *
+        * @param updateInterval
+        *            The update interval of this reaction (in milliseconds)
+        * @return This reaction
+        */
+       public Reaction setUpdateInterval(long updateInterval) {
+               this.updateInterval = updateInterval;
+               return this;
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/rhynodge/State.java b/src/main/java/net/pterodactylus/rhynodge/State.java
new file mode 100644 (file)
index 0000000..fe7f8ea
--- /dev/null
@@ -0,0 +1,77 @@
+/*
+ * Rhynodge - State.java - Copyright © 2013 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.rhynodge;
+
+/**
+ * Defines the current state of a system.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public interface State {
+
+       /**
+        * Returns the time when this state was retrieved.
+        *
+        * @return The time when this state was retrieved (in millseconds since Jan
+        *         1, 1970 UTC)
+        */
+       long time();
+
+       /**
+        * Whether the state was successfully retrieved. This method should only
+        * return {@code true} if a meaningful result could be retrieved; if e. g. a
+        * service is currently not reachable, this method should return false
+        * instead of emulating success by using empty lists or similar constructs.
+        *
+        * @return {@code true} if the state could be retrieved successfully,
+        *         {@code false} otherwise
+        */
+       boolean success();
+
+       /**
+        * Returns the number of consecutive failures. This method only returns a
+        * meaningful number iff {@link #success()} returns {@code false}. If
+        * {@link #success()} returns {@code false} for the first time after
+        * returning {@code true} and this method is called after {@link #success()}
+        * it will return {@code 1}.
+        *
+        * @return The number of consecutive failures
+        */
+       int failCount();
+
+       /**
+        * Sets the fail count of this state.
+        *
+        * @param failCount
+        *            The fail count of this state
+        */
+       void setFailCount(int failCount);
+
+       /**
+        * If {@link #success()} returns {@code false}, this method may return a
+        * {@link Throwable} to give some details for the reason why retrieving the
+        * state was not possible. For example, network-based {@link Query}s might
+        * return any exception that were encountered while communicating with the
+        * remote service.
+        *
+        * @return An exception that occured, may be {@code null} in case an
+        *         exception can not be meaningfully returned
+        */
+       Throwable exception();
+
+}
diff --git a/src/main/java/net/pterodactylus/rhynodge/Trigger.java b/src/main/java/net/pterodactylus/rhynodge/Trigger.java
new file mode 100644 (file)
index 0000000..3b5a730
--- /dev/null
@@ -0,0 +1,55 @@
+/*
+ * Rhynodge - Trigger.java - Copyright © 2013 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.rhynodge;
+
+import net.pterodactylus.rhynodge.output.Output;
+import net.pterodactylus.rhynodge.states.FileState;
+
+/**
+ * A trigger determines whether two different states actually warrant a change
+ * trigger. For example, two {@link FileState}s might contain different file
+ * sizes but a trigger might only care about whether the file appeared or
+ * disappeared since the last check.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public interface Trigger {
+
+       /**
+        * Checks whether the given states warrant a change trigger.
+        *
+        * @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,
+        *         {@code false} otherwise
+        */
+       boolean triggers(State currentState, State previousState);
+
+       /**
+        * Returns the output of this trigger. This will only return a meaningful
+        * value if {@link #triggers(State, State)} returns {@code true}.
+        *
+        * @param reaction
+        *            The reaction being triggered
+        * @return The output of this trigger
+        */
+       Output output(Reaction reaction);
+
+}
diff --git a/src/main/java/net/pterodactylus/rhynodge/actions/EmailAction.java b/src/main/java/net/pterodactylus/rhynodge/actions/EmailAction.java
new file mode 100644 (file)
index 0000000..6e866c8
--- /dev/null
@@ -0,0 +1,103 @@
+/*
+ * Rhynodge - EmailAction.java - Copyright © 2013 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.rhynodge.actions;
+
+import java.util.Properties;
+
+import javax.mail.Message.RecipientType;
+import javax.mail.MessagingException;
+import javax.mail.Session;
+import javax.mail.Transport;
+import javax.mail.internet.InternetAddress;
+import javax.mail.internet.MimeBodyPart;
+import javax.mail.internet.MimeMessage;
+import javax.mail.internet.MimeMultipart;
+
+import net.pterodactylus.rhynodge.Action;
+import net.pterodactylus.rhynodge.output.Output;
+
+/**
+ * {@link Action} implementation that sends an email containing the triggering
+ * object to an email address.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class EmailAction implements Action {
+
+       /** The name of the SMTP host. */
+       private final String hostname;
+
+       /** The email address of the sender. */
+       private final String sender;
+
+       /** The email address of the recipient. */
+       private final String recipient;
+
+       /**
+        * Creates a new email action.
+        *
+        * @param hostname
+        *            The hostname of the SMTP server
+        * @param sender
+        *            The email address of the sender
+        * @param recipient
+        *            The email address of the recipient
+        */
+       public EmailAction(String hostname, String sender, String recipient) {
+               this.hostname = hostname;
+               this.sender = sender;
+               this.recipient = recipient;
+       }
+
+       //
+       // ACTION METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public void execute(Output output) {
+               Properties properties = System.getProperties();
+               properties.put("mail.smtp.host", hostname);
+               Session session = Session.getInstance(properties);
+               MimeMessage message = new MimeMessage(session);
+               try {
+                       /* create message. */
+                       message.setFrom(new InternetAddress(sender));
+                       message.setRecipient(RecipientType.TO, new InternetAddress(recipient));
+                       message.setSubject(output.summary());
+
+                       /* create text and html parts. */
+                       MimeMultipart multipart = new MimeMultipart();
+                       multipart.setSubType("alternative");
+                       MimeBodyPart textPart = new MimeBodyPart();
+                       textPart.setContent(output.text("text/plain", -1), "text/plain;charset=utf-8");
+                       MimeBodyPart htmlPart = new MimeBodyPart();
+                       htmlPart.setContent(output.text("text/html", -1), "text/html;charset=utf-8");
+                       multipart.addBodyPart(textPart);
+                       multipart.addBodyPart(htmlPart);
+                       message.setContent(multipart);
+
+                       Transport.send(message);
+               } catch (MessagingException me1) {
+                       /* swallow. */
+               }
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/rhynodge/actions/StandardOutAction.java b/src/main/java/net/pterodactylus/rhynodge/actions/StandardOutAction.java
new file mode 100644 (file)
index 0000000..5791ecb
--- /dev/null
@@ -0,0 +1,39 @@
+/*
+ * Rhynodge - StandardOutAction.java - Copyright © 2013 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.rhynodge.actions;
+
+import net.pterodactylus.rhynodge.Action;
+import net.pterodactylus.rhynodge.State;
+import net.pterodactylus.rhynodge.output.Output;
+
+/**
+ * {@link Action} that simply dumps all {@link State}s to standard output.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class StandardOutAction implements Action {
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public void execute(Output output) {
+               System.out.println(String.format("Triggered by %s.", output.text("text/plain", -1)));
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/rhynodge/engine/Engine.java b/src/main/java/net/pterodactylus/rhynodge/engine/Engine.java
new file mode 100644 (file)
index 0000000..9e4d883
--- /dev/null
@@ -0,0 +1,211 @@
+/*
+ * Rhynodge - Engine.java - Copyright © 2013 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.rhynodge.engine;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.SortedMap;
+
+import net.pterodactylus.rhynodge.Filter;
+import net.pterodactylus.rhynodge.Query;
+import net.pterodactylus.rhynodge.Reaction;
+import net.pterodactylus.rhynodge.Trigger;
+import net.pterodactylus.rhynodge.states.AbstractState;
+import net.pterodactylus.rhynodge.states.FailedState;
+import net.pterodactylus.rhynodge.states.StateManager;
+
+import org.apache.commons.lang3.tuple.Pair;
+import org.apache.log4j.Logger;
+
+import com.google.common.collect.Maps;
+import com.google.common.util.concurrent.AbstractExecutionThreadService;
+
+/**
+ * Rhynodge main engine.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class Engine extends AbstractExecutionThreadService {
+
+       /** The logger. */
+       private static final Logger logger = Logger.getLogger(Engine.class);
+
+       /** The state manager. */
+       private final StateManager stateManager;
+
+       /** All defined reactions. */
+       /* synchronize on itself. */
+       private final Map<String, Reaction> reactions = new HashMap<String, Reaction>();
+
+       /**
+        * Creates a new engine.
+        *
+        * @param stateManager
+        *            The state manager
+        */
+       public Engine(StateManager stateManager) {
+               this.stateManager = stateManager;
+       }
+
+       //
+       // ACCESSORS
+       //
+
+       /**
+        * Adds the given reaction to this engine.
+        *
+        * @param name
+        *            The name of the reaction
+        * @param reaction
+        *            The reaction to add to this engine
+        */
+       public void addReaction(String name, Reaction reaction) {
+               synchronized (reactions) {
+                       reactions.put(name, reaction);
+                       reactions.notifyAll();
+               }
+       }
+
+       /**
+        * Removes the reaction with the given name.
+        *
+        * @param name
+        *            The name of the reaction to remove
+        */
+       public void removeReaction(String name) {
+               synchronized (reactions) {
+                       if (!reactions.containsKey(name)) {
+                               return;
+                       }
+                       reactions.remove(name);
+                       reactions.notifyAll();
+               }
+       }
+
+       //
+       // ABSTRACTSERVICE METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public void run() {
+               while (isRunning()) {
+
+                       /* delay if we have no reactions. */
+                       synchronized (reactions) {
+                               if (reactions.isEmpty()) {
+                                       logger.debug("Sleeping while no Reactions available.");
+                                       try {
+                                               reactions.wait();
+                                       } catch (InterruptedException ie1) {
+                                               /* ignore, we’re looping anyway. */
+                                       }
+                                       continue;
+                               }
+                       }
+
+                       /* find next reaction. */
+                       SortedMap<Long, Pair<String, Reaction>> nextReactions = Maps.newTreeMap();
+                       String reactionName;
+                       Reaction nextReaction;
+                       synchronized (reactions) {
+                               for (Entry<String, Reaction> reactionEntry : reactions.entrySet()) {
+                                       net.pterodactylus.rhynodge.State state = stateManager.loadLastState(reactionEntry.getKey());
+                                       long stateTime = (state != null) ? state.time() : 0;
+                                       nextReactions.put(stateTime + reactionEntry.getValue().updateInterval(), Pair.of(reactionEntry.getKey(), reactionEntry.getValue()));
+                               }
+                               reactionName = nextReactions.get(nextReactions.firstKey()).getLeft();
+                               nextReaction = nextReactions.get(nextReactions.firstKey()).getRight();
+                       }
+                       logger.debug(String.format("Next Reaction: %s.", reactionName));
+
+                       /* wait until the next reaction has to run. */
+                       net.pterodactylus.rhynodge.State lastState = stateManager.loadLastState(reactionName);
+                       long lastStateTime = (lastState != null) ? lastState.time() : 0;
+                       int lastStateFailCount = (lastState != null) ? lastState.failCount() : 0;
+                       long waitTime = (lastStateTime + nextReaction.updateInterval()) - System.currentTimeMillis();
+                       logger.debug(String.format("Time to wait for next Reaction: %d millseconds.", waitTime));
+                       if (waitTime > 0) {
+                               synchronized (reactions) {
+                                       try {
+                                               logger.info(String.format("Waiting until %tc.", lastStateTime + nextReaction.updateInterval()));
+                                               reactions.wait(waitTime);
+                                       } catch (InterruptedException ie1) {
+                                               /* we’re looping! */
+                                       }
+                               }
+
+                               /* re-start loop to check for new reactions. */
+                               continue;
+                       }
+
+                       /* run reaction. */
+                       logger.info(String.format("Running Query for %s...", reactionName));
+                       Query query = nextReaction.query();
+                       net.pterodactylus.rhynodge.State state;
+                       try {
+                               logger.debug("Querying system...");
+                               state = query.state();
+                               if (state == null) {
+                                       state = FailedState.INSTANCE;
+                               }
+                               logger.debug("System queried.");
+                       } catch (Throwable t1) {
+                               logger.warn("Querying system failed!", t1);
+                               state = new AbstractState(t1) {
+                                       /* no further state. */
+                               };
+                       }
+                       logger.debug(String.format("State is %s.", state));
+
+                       /* convert states. */
+                       for (Filter filter : nextReaction.filters()) {
+                               if (state.success()) {
+                                       net.pterodactylus.rhynodge.State newState = filter.filter(state);
+                                       logger.debug(String.format("Old state is %s, new state is %s.", state, newState));
+                                       state = newState;
+                               }
+                       }
+                       if (!state.success()) {
+                               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();
+                       boolean triggerHit = false;
+                       if ((lastSuccessfulState != null) && lastSuccessfulState.success() && state.success()) {
+                               logger.debug("Checking Trigger for changes...");
+                               triggerHit = trigger.triggers(state, lastSuccessfulState);
+                       }
+
+                       /* run action if trigger was hit. */
+                       logger.debug(String.format("Trigger was hit: %s.", triggerHit));
+                       if (triggerHit) {
+                               logger.info("Executing Action...");
+                               nextReaction.action().execute(trigger.output(nextReaction));
+                       }
+
+               }
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/rhynodge/engine/Starter.java b/src/main/java/net/pterodactylus/rhynodge/engine/Starter.java
new file mode 100644 (file)
index 0000000..a5d79ac
--- /dev/null
@@ -0,0 +1,83 @@
+/*
+ * Rhynodge - Starter.java - Copyright © 2013 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.rhynodge.engine;
+
+import net.pterodactylus.rhynodge.loader.ChainWatcher;
+import net.pterodactylus.rhynodge.states.StateManager;
+
+import com.lexicalscope.jewel.cli.CliFactory;
+import com.lexicalscope.jewel.cli.Option;
+
+/**
+ * Rhynodge main starter class.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class Starter {
+
+       /**
+        * JVM main entry method.
+        *
+        * @param arguments
+        *            Command-line arguments
+        */
+       public static void main(String... arguments) {
+
+               /* parse command line. */
+               Parameters parameters = CliFactory.parseArguments(Parameters.class, arguments);
+
+               /* create the state manager. */
+               StateManager stateManager = new StateManager(parameters.getStateDirectory());
+
+               /* create the engine. */
+               Engine engine = new Engine(stateManager);
+
+               /* start a watcher. */
+               ChainWatcher chainWatcher = new ChainWatcher(engine, parameters.getChainDirectory());
+               chainWatcher.start();
+
+               /* start the engine. */
+               engine.start();
+       }
+
+       /**
+        * Definition of the command-line parameters.
+        *
+        * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+        */
+       private static interface Parameters {
+
+               /**
+                * Returns the directory to watch for chains.
+                *
+                * @return The chain directory
+                */
+               @Option(defaultValue = "chains", shortName = "c", description = "The directory to watch for chains")
+               String getChainDirectory();
+
+               /**
+                * Returns the directory to store states in.
+                *
+                * @return The states directory
+                */
+               @Option(defaultValue = "states", shortName = "s", description = "The directory to store states in")
+               String getStateDirectory();
+
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/rhynodge/filters/EpisodeFilter.java b/src/main/java/net/pterodactylus/rhynodge/filters/EpisodeFilter.java
new file mode 100644 (file)
index 0000000..91fe098
--- /dev/null
@@ -0,0 +1,102 @@
+/*
+ * Rhynodge - EpisodeFilter.java - Copyright © 2013 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.rhynodge.filters;
+
+import static com.google.common.base.Preconditions.checkState;
+
+import java.util.LinkedHashMap;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import net.pterodactylus.rhynodge.Filter;
+import net.pterodactylus.rhynodge.State;
+import net.pterodactylus.rhynodge.states.EpisodeState;
+import net.pterodactylus.rhynodge.states.EpisodeState.Episode;
+import net.pterodactylus.rhynodge.states.FailedState;
+import net.pterodactylus.rhynodge.states.TorrentState;
+import net.pterodactylus.rhynodge.states.TorrentState.TorrentFile;
+
+/**
+ * {@link Filter} implementation that extracts {@link Episode} information from
+ * the {@link TorrentFile}s contained in a {@link TorrentState}.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class EpisodeFilter implements Filter {
+
+       /** The pattern to parse episode information from the filename. */
+       private static Pattern episodePattern = Pattern.compile("S(\\d{2})E(\\d{2})|[^\\d](\\d{1,2})x(\\d{2})[^\\d]");
+
+       //
+       // FILTER METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public State filter(State state) {
+               if (!state.success()) {
+                       return FailedState.from(state);
+               }
+               checkState(state instanceof TorrentState, "state is not a TorrentState but a %s!", state.getClass());
+
+               TorrentState torrentState = (TorrentState) state;
+               LinkedHashMap<Episode, Episode> episodes = new LinkedHashMap<Episode, Episode>();
+               for (TorrentFile torrentFile : torrentState) {
+                       Episode episode = extractEpisode(torrentFile);
+                       if (episode == null) {
+                               continue;
+                       }
+                       episodes.put(episode, episode);
+                       episode = episodes.get(episode);
+                       episode.addTorrentFile(torrentFile);
+               }
+
+               return new EpisodeState(episodes.values());
+       }
+
+       //
+       // STATIC METHODS
+       //
+
+       /**
+        * Extracts episode information from the given torrent file.
+        *
+        * @param torrentFile
+        *            The torrent file to extract the episode information from
+        * @return The extracted episode information, or {@code null} if no episode
+        *         information could be found
+        */
+       private static Episode extractEpisode(TorrentFile torrentFile) {
+               Matcher matcher = episodePattern.matcher(torrentFile.name());
+               if (!matcher.find()) {
+                       return null;
+               }
+               String seasonString = matcher.group(1);
+               String episodeString = matcher.group(2);
+               if ((seasonString == null) && (episodeString == null)) {
+                       seasonString = matcher.group(3);
+                       episodeString = matcher.group(4);
+               }
+               int season = Integer.valueOf(seasonString);
+               int episode = Integer.valueOf(episodeString);
+               return new Episode(season, episode);
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/rhynodge/filters/HtmlFilter.java b/src/main/java/net/pterodactylus/rhynodge/filters/HtmlFilter.java
new file mode 100644 (file)
index 0000000..d63f563
--- /dev/null
@@ -0,0 +1,51 @@
+/*
+ * Rhynodge - HtmlFilter.java - Copyright © 2013 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.rhynodge.filters;
+
+import static com.google.common.base.Preconditions.checkState;
+
+import net.pterodactylus.rhynodge.Filter;
+import net.pterodactylus.rhynodge.State;
+import net.pterodactylus.rhynodge.states.FailedState;
+import net.pterodactylus.rhynodge.states.HtmlState;
+import net.pterodactylus.rhynodge.states.HttpState;
+
+import org.jsoup.Jsoup;
+import org.jsoup.nodes.Document;
+
+/**
+ * {@link Filter} that converts a {@link HttpState} into an {@link HtmlState}.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class HtmlFilter implements Filter {
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public State filter(State state) {
+               if (!state.success()) {
+                       return FailedState.from(state);
+               }
+               checkState(state instanceof HttpState, "state is not a HttpState but a %s", state.getClass().getName());
+               Document document = Jsoup.parse(((HttpState) state).content(), ((HttpState) state).uri());
+               return new HtmlState(((HttpState) state).uri(), document);
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/rhynodge/filters/KickAssTorrentsFilter.java b/src/main/java/net/pterodactylus/rhynodge/filters/KickAssTorrentsFilter.java
new file mode 100644 (file)
index 0000000..11865e7
--- /dev/null
@@ -0,0 +1,168 @@
+/*
+ * Rhynodge - KickAssTorrentsFilter.java - Copyright © 2013 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.rhynodge.filters;
+
+import static com.google.common.base.Preconditions.checkState;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+
+import net.pterodactylus.rhynodge.Filter;
+import net.pterodactylus.rhynodge.State;
+import net.pterodactylus.rhynodge.queries.HttpQuery;
+import net.pterodactylus.rhynodge.states.FailedState;
+import net.pterodactylus.rhynodge.states.HtmlState;
+import net.pterodactylus.rhynodge.states.TorrentState;
+import net.pterodactylus.rhynodge.states.TorrentState.TorrentFile;
+
+import org.jsoup.nodes.Document;
+import org.jsoup.nodes.Element;
+import org.jsoup.select.Elements;
+
+/**
+ * {@link Filter} implementation that parses a {@link TorrentState} from an
+ * {@link HtmlState} which was generated by a {@link HttpQuery} to
+ * {@code kickasstorrents.ph}.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class KickAssTorrentsFilter implements Filter {
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public State filter(State state) {
+               if (!state.success()) {
+                       return FailedState.from(state);
+               }
+               checkState(state instanceof HtmlState, "state is not an HtmlState but a %s", state.getClass().getName());
+
+               /* get result table. */
+               Document document = ((HtmlState) state).document();
+               Elements mainTable = document.select("table.data");
+               if (mainTable.isEmpty()) {
+                       /* no main table? */
+                       return new FailedState();
+               }
+
+               /* iterate over all rows. */
+               TorrentState torrentState = new TorrentState();
+               Elements dataRows = mainTable.select("tr:gt(0)");
+               for (Element dataRow : dataRows) {
+                       String name = extractName(dataRow);
+                       String size = extractSize(dataRow);
+                       String magnetUri = extractMagnetUri(dataRow);
+                       String downloadUri;
+                       int fileCount = extractFileCount(dataRow);
+                       int seedCount = extractSeedCount(dataRow);
+                       int leechCount = extractLeechCount(dataRow);
+                       try {
+                               downloadUri = new URI(((HtmlState) state).uri()).resolve(extractDownloadUri(dataRow)).toString();
+                               TorrentFile torrentFile = new TorrentFile(name, size, magnetUri, downloadUri, fileCount, seedCount, leechCount);
+                               torrentState.addTorrentFile(torrentFile);
+                       } catch (URISyntaxException use1) {
+                               /* ignore; if uri was wrong, we wouldn’t be here. */
+                       }
+               }
+
+               return torrentState;
+       }
+
+       //
+       // STATIC METHODS
+       //
+
+       /**
+        * Extracts the name from the given row.
+        *
+        * @param dataRow
+        *            The row to extract the name from
+        * @return The extracted name
+        */
+       private static String extractName(Element dataRow) {
+               return dataRow.select("div.torrentname a.normalgrey").text();
+       }
+
+       /**
+        * Extracts the size from the given row.
+        *
+        * @param dataRow
+        *            The row to extract the size from
+        * @return The extracted size
+        */
+       private static String extractSize(Element dataRow) {
+               return dataRow.select("td:eq(1)").text();
+       }
+
+       /**
+        * Extracts the magnet URI from the given row.
+        *
+        * @param dataRow
+        *            The row to extract the magnet URI from
+        * @return The extracted magnet URI
+        */
+       private static String extractMagnetUri(Element dataRow) {
+               return dataRow.select("a.imagnet").attr("href");
+       }
+
+       /**
+        * Extracts the download URI from the given row.
+        *
+        * @param dataRow
+        *            The row to extract the download URI from
+        * @return The extracted download URI
+        */
+       private static String extractDownloadUri(Element dataRow) {
+               return dataRow.select("a.idownload:not(.partner1Button)").attr("href");
+       }
+
+       /**
+        * Extracts the file count from the given row.
+        *
+        * @param dataRow
+        *            The row to extract the file count from
+        * @return The extracted file count
+        */
+       private static int extractFileCount(Element dataRow) {
+               return Integer.valueOf(dataRow.select("td:eq(2)").text());
+       }
+
+       /**
+        * Extracts the seed count from the given row.
+        *
+        * @param dataRow
+        *            The row to extract the seed count from
+        * @return The extracted seed count
+        */
+       private static int extractSeedCount(Element dataRow) {
+               return Integer.valueOf(dataRow.select("td:eq(4)").text());
+       }
+
+       /**
+        * Extracts the leech count from the given row.
+        *
+        * @param dataRow
+        *            The row to extract the leech count from
+        * @return The extracted leech count
+        */
+       private static int extractLeechCount(Element dataRow) {
+               return Integer.valueOf(dataRow.select("td:eq(5)").text());
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/rhynodge/loader/Chain.java b/src/main/java/net/pterodactylus/rhynodge/loader/Chain.java
new file mode 100644 (file)
index 0000000..8fc6c35
--- /dev/null
@@ -0,0 +1,314 @@
+/*
+ * Rhynodge - Chain.java - Copyright © 2013 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.rhynodge.loader;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * Model for chain definitions.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class Chain {
+
+       /**
+        * Parameter model.
+        *
+        * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+        */
+       public static class Parameter {
+
+               /** The name of the parameter. */
+               @JsonProperty
+               private String name;
+
+               /** The value of the parameter. */
+               @JsonProperty
+               private String value;
+
+               /**
+                * Returns the name of the parameter.
+                *
+                * @return The name of the parameter
+                */
+               public String name() {
+                       return name;
+               }
+
+               /**
+                * Returns the value of the parameter.
+                *
+                * @return The value of the parameter
+                */
+               public String value() {
+                       return value;
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               @Override
+               public int hashCode() {
+                       int hashCode = 0;
+                       hashCode ^= name.hashCode();
+                       hashCode ^= value.hashCode();
+                       return hashCode;
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               @Override
+               public boolean equals(Object object) {
+                       if (!(object instanceof Parameter)) {
+                               return false;
+                       }
+                       Parameter parameter = (Parameter) object;
+                       if (!name.equals(parameter.name)) {
+                               return false;
+                       }
+                       if (!value.equals(parameter.value)) {
+                               return false;
+                       }
+                       return true;
+               }
+
+       }
+
+       /**
+        * Defines a part of a chain.
+        *
+        * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+        */
+       public static class Part {
+
+               /** The class name of the part. */
+               @JsonProperty(value = "class")
+               private String name;
+
+               /** The parameters of the part. */
+               @JsonProperty
+               private List<Parameter> parameters = new ArrayList<Parameter>();
+
+               /**
+                * Returns the name of the part’s class.
+                *
+                * @return The name of the part’s class
+                */
+               public String name() {
+                       return name;
+               }
+
+               /**
+                * Returns the parameters of the part.
+                *
+                * @return The parameters of the part
+                */
+               public List<Parameter> parameters() {
+                       return parameters;
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               @Override
+               public int hashCode() {
+                       int hashCode = 0;
+                       hashCode ^= name.hashCode();
+                       for (Parameter parameter : parameters) {
+                               hashCode ^= parameter.hashCode();
+                       }
+                       return hashCode;
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               @Override
+               public boolean equals(Object object) {
+                       if (!(object instanceof Part)) {
+                               return false;
+                       }
+                       Part part = (Part) object;
+                       if (!name.equals(part.name)) {
+                               return false;
+                       }
+                       if (parameters.size() != part.parameters.size()) {
+                               return false;
+                       }
+                       for (int parameterIndex = 0; parameterIndex < parameters.size(); ++parameterIndex) {
+                               if (!parameters.get(parameterIndex).equals(part.parameters.get(parameterIndex))) {
+                                       return false;
+                               }
+                       }
+                       return true;
+               }
+
+       }
+
+       /** Whether this chain is enabled. */
+       @JsonProperty
+       private boolean enabled;
+
+       /** The name of the chain. */
+       @JsonProperty
+       private String name;
+
+       /** The query of the chain. */
+       @JsonProperty
+       private Part query;
+
+       /** The filters of the chain. */
+       @JsonProperty
+       private List<Part> filters = new ArrayList<Part>();
+
+       /** The trigger of the chain. */
+       @JsonProperty
+       private Part trigger;
+
+       /** The action of the chain. */
+       @JsonProperty
+       private Part action;
+
+       /** Interval between updates (in seconds). */
+       @JsonProperty
+       private int updateInterval;
+
+       /**
+        * Returns whether this chain is enabled.
+        *
+        * @return {@code true} if this chain is enabled, {@code false} otherwise
+        */
+       public boolean enabled() {
+               return enabled;
+       }
+
+       /**
+        * Returns the name of the chain.
+        *
+        * @return The name of the chain
+        */
+       public String name() {
+               return name;
+       }
+
+       /**
+        * Returns the query of this chain.
+        *
+        * @return The query of this chain
+        */
+       public Part query() {
+               return query;
+       }
+
+       /**
+        * Returns the filters of this chain.
+        *
+        * @return The filters of this chain
+        */
+       public List<Part> filters() {
+               return filters;
+       }
+
+       /**
+        * Returns the trigger of this chain.
+        *
+        * @return The trigger of this chain
+        */
+       public Part trigger() {
+               return trigger;
+       }
+
+       /**
+        * Returns the action of this chain.
+        *
+        * @return The action of this chain
+        */
+       public Part action() {
+               return action;
+       }
+
+       /**
+        * Returns the update interval of the chain.
+        *
+        * @return The update interval (in seconds)
+        */
+       public int updateInterval() {
+               return updateInterval;
+       }
+
+       //
+       // OBJECT METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public int hashCode() {
+               int hashCode = 0;
+               hashCode ^= name.hashCode();
+               hashCode ^= query.hashCode();
+               for (Part filter : filters) {
+                       hashCode ^= filter.hashCode();
+               }
+               hashCode ^= trigger.hashCode();
+               hashCode ^= action.hashCode();
+               hashCode ^= updateInterval;
+               return hashCode;
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public boolean equals(Object object) {
+               if (!(object instanceof Chain)) {
+                       return false;
+               }
+               Chain chain = (Chain) object;
+               if (!name.equals(chain.name)) {
+                       return false;
+               }
+               if (!query.equals(chain.query)) {
+                       return false;
+               }
+               if (filters.size() != chain.filters.size()) {
+                       return false;
+               }
+               for (int filterIndex = 0; filterIndex < filters.size(); ++filterIndex) {
+                       if (!filters.get(filterIndex).equals(chain.filters.get(filterIndex))) {
+                               return false;
+                       }
+               }
+               if (!trigger.equals(chain.trigger)) {
+                       return false;
+               }
+               if (!action.equals(chain.action)) {
+                       return false;
+               }
+               if (updateInterval != chain.updateInterval) {
+                       return false;
+               }
+               return true;
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/rhynodge/loader/ChainWatcher.java b/src/main/java/net/pterodactylus/rhynodge/loader/ChainWatcher.java
new file mode 100644 (file)
index 0000000..52b3fa4
--- /dev/null
@@ -0,0 +1,234 @@
+/*
+ * Rhynodge - ChainWatcher.java - Copyright © 2013 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.rhynodge.loader;
+
+import java.io.File;
+import java.io.FilenameFilter;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+import net.pterodactylus.rhynodge.Reaction;
+import net.pterodactylus.rhynodge.engine.Engine;
+import net.pterodactylus.rhynodge.loader.Chain.Parameter;
+import net.pterodactylus.rhynodge.loader.Chain.Part;
+
+import org.apache.log4j.Logger;
+
+import com.fasterxml.jackson.core.JsonParseException;
+import com.fasterxml.jackson.databind.JsonMappingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.base.Predicate;
+import com.google.common.collect.Maps;
+import com.google.common.util.concurrent.AbstractExecutionThreadService;
+import com.google.common.util.concurrent.Uninterruptibles;
+
+/**
+ * Watches a directory for chain configuration files and loads and unloads
+ * {@link Reaction}s from the {@link Engine}.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class ChainWatcher extends AbstractExecutionThreadService {
+
+       /** The logger. */
+       private static final Logger logger = Logger.getLogger(ChainWatcher.class);
+
+       /** The JSON object mapper. */
+       private static final ObjectMapper objectMapper = new ObjectMapper();
+
+       /** The reaction loader. */
+       private final ReactionLoader reactionLoader = new ReactionLoader();
+
+       /** The engine to load reactions with. */
+       private final Engine engine;
+
+       /** The directory to watch for chain configuration files. */
+       private final String directory;
+
+       /**
+        * Creates a new chain watcher.
+        *
+        * @param engine
+        *            The engine to load reactions with
+        * @param directory
+        *            The directory to watch
+        */
+       public ChainWatcher(Engine engine, String directory) {
+               this.engine = engine;
+               this.directory = directory;
+       }
+
+       //
+       // ABSTRACTEXECUTIONTHREADSERVICE METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected void run() throws Exception {
+
+               /* loaded chains. */
+               final Map<String, Chain> loadedChains = new HashMap<String, Chain>();
+
+               while (isRunning()) {
+
+                       /* check if directory is there. */
+                       File directoryFile = new File(directory);
+                       if (!directoryFile.exists() || !directoryFile.isDirectory() || !directoryFile.canRead()) {
+                               Uninterruptibles.sleepUninterruptibly(1, TimeUnit.SECONDS);
+                               continue;
+                       }
+
+                       /* list all files, scan for configuration files. */
+                       logger.debug(String.format("Scanning %s...", directory));
+                       File[] configurationFiles = directoryFile.listFiles(new FilenameFilter() {
+
+                               @Override
+                               public boolean accept(File dir, String name) {
+                                       return name.endsWith(".json");
+                               }
+                       });
+                       logger.debug(String.format("Found %d configuration file(s), parsing...", configurationFiles.length));
+
+                       /* now parse all XML files. */
+                       Map<String, Chain> chains = new HashMap<String, Chain>();
+                       for (File configurationFile : configurationFiles) {
+
+                               /* parse XML file. */
+                               Chain chain = parseConfigurationFile(configurationFile);
+                               if (chain == null) {
+                                       logger.warn(String.format("Could not parse %s.", configurationFile));
+                                       continue;
+                               }
+
+                               /* dump chain */
+                               logger.debug(String.format(" Enabled: %s", chain.enabled()));
+
+                               logger.debug(String.format(" Query: %s", chain.query().name()));
+                               for (Parameter parameter : chain.query().parameters()) {
+                                       logger.debug(String.format("  Parameter: %s=%s", parameter.name(), parameter.value()));
+                               }
+                               for (Part filter : chain.filters()) {
+                                       logger.debug(String.format(" Filter: %s", filter.name()));
+                                       for (Parameter parameter : filter.parameters()) {
+                                               logger.debug(String.format("  Parameter: %s=%s", parameter.name(), parameter.value()));
+                                       }
+                               }
+                               logger.debug(String.format(" Trigger: %s", chain.trigger().name()));
+                               for (Parameter parameter : chain.trigger().parameters()) {
+                                       logger.debug(String.format("  Parameter: %s=%s", parameter.name(), parameter.value()));
+                               }
+                               logger.debug(String.format(" Action: %s", chain.action().name()));
+                               for (Parameter parameter : chain.action().parameters()) {
+                                       logger.debug(String.format("  Parameter: %s=%s", parameter.name(), parameter.value()));
+                               }
+
+                               chains.put(getReactionName(configurationFile.getName()), chain);
+                       }
+
+                       /* filter enabled chains. */
+                       Map<String, Chain> enabledChains = Maps.filterEntries(chains, new Predicate<Entry<String, Chain>>() {
+
+                               @Override
+                               public boolean apply(Entry<String, Chain> chainEntry) {
+                                       return chainEntry.getValue().enabled();
+                               }
+                       });
+                       logger.debug(String.format("Found %d enabled Chain(s).", enabledChains.size()));
+
+                       /* check for removed chains. */
+                       Set<String> chainsToRemove = new HashSet<String>();
+                       for (Entry<String, Chain> loadedChain : loadedChains.entrySet()) {
+
+                               /* skip chains that still exist. */
+                               if (enabledChains.containsKey(loadedChain.getKey())) {
+                                       continue;
+                               }
+
+                               logger.info(String.format("Removing Chain: %s", loadedChain.getKey()));
+                               engine.removeReaction(loadedChain.getKey());
+                               chainsToRemove.add(loadedChain.getKey());
+                       }
+
+                       /* remove removed chains from loaded chains. */
+                       for (String reactionName : chainsToRemove) {
+                               loadedChains.remove(reactionName);
+                       }
+
+                       /* check for new chains. */
+                       for (Entry<String, Chain> enabledChain : enabledChains.entrySet()) {
+
+                               /* skip already loaded chains. */
+                               if (loadedChains.containsValue(enabledChain.getValue())) {
+                                       continue;
+                               }
+
+                               logger.info(String.format("Loading new Chain: %s", enabledChain.getKey()));
+
+                               Reaction reaction = reactionLoader.loadReaction(enabledChain.getValue());
+                               engine.addReaction(enabledChain.getKey(), reaction);
+                               loadedChains.put(enabledChain.getKey(), enabledChain.getValue());
+                       }
+
+                       /* wait before checking again. */
+                       Uninterruptibles.sleepUninterruptibly(5, TimeUnit.SECONDS);
+               }
+       }
+
+       //
+       // STATIC METHODS
+       //
+
+       /**
+        * Parses the given configuration file into a {@link Chain}.
+        *
+        * @param configurationFile
+        *            The configuration file to parse
+        * @return The parsed chain
+        */
+       private static Chain parseConfigurationFile(File configurationFile) {
+               try {
+                       return objectMapper.readValue(configurationFile, Chain.class);
+               } catch (JsonParseException jpe1) {
+                       logger.warn(String.format("Could not parse %s.", configurationFile), jpe1);
+               } catch (JsonMappingException jme1) {
+                       logger.warn(String.format("Could not parse %s.", configurationFile), jme1);
+               } catch (IOException ioe1) {
+                       logger.info(String.format("Could not read %s.", configurationFile));
+               }
+               return null;
+       }
+
+       /**
+        * Extracts the name of the reaction from the given filename.
+        *
+        * @param filename
+        *            The filename to extract the reaction name from
+        * @return The name of the reaction
+        */
+       private static String getReactionName(String filename) {
+               return (filename.lastIndexOf(".") > -1) ? filename.substring(0, filename.lastIndexOf(".")) : filename;
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/rhynodge/loader/LoaderException.java b/src/main/java/net/pterodactylus/rhynodge/loader/LoaderException.java
new file mode 100644 (file)
index 0000000..badaadf
--- /dev/null
@@ -0,0 +1,66 @@
+/*
+ * Rhynodge - LoaderException.java - Copyright © 2013 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.rhynodge.loader;
+
+/**
+ * Exception that signals a problem when loading chain XML files.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class LoaderException extends Exception {
+
+       /**
+        * Creates a new loader exception.
+        */
+       public LoaderException() {
+               super();
+       }
+
+       /**
+        * Creates a new loader exception.
+        *
+        * @param message
+        *            The message of the exception
+        */
+       public LoaderException(String message) {
+               super(message);
+       }
+
+       /**
+        * Creates a new loader exception.
+        *
+        * @param throwable
+        *            The root cause
+        */
+       public LoaderException(Throwable throwable) {
+               super(throwable);
+       }
+
+       /**
+        * Creates a new loader exception.
+        *
+        * @param message
+        *            The message of the exception
+        * @param throwable
+        *            The root cause
+        */
+       public LoaderException(String message, Throwable throwable) {
+               super(message, throwable);
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/rhynodge/loader/ReactionLoader.java b/src/main/java/net/pterodactylus/rhynodge/loader/ReactionLoader.java
new file mode 100644 (file)
index 0000000..2cf1d21
--- /dev/null
@@ -0,0 +1,177 @@
+/*
+ * Rhynodge - Loader.java - Copyright © 2013 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.rhynodge.loader;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+import net.pterodactylus.rhynodge.Action;
+import net.pterodactylus.rhynodge.Filter;
+import net.pterodactylus.rhynodge.Query;
+import net.pterodactylus.rhynodge.Reaction;
+import net.pterodactylus.rhynodge.Trigger;
+import net.pterodactylus.rhynodge.loader.Chain.Parameter;
+import net.pterodactylus.rhynodge.loader.Chain.Part;
+
+/**
+ * Creates {@link Reaction}s from {@link Chain}s.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class ReactionLoader {
+
+       /**
+        * Creates a {@link Reaction} from the given {@link Chain}.
+        *
+        * @param chain
+        *            The chain to create a reaction from
+        * @return The created reaction
+        * @throws LoaderException
+        *             if a class can not be loaded
+        */
+       @SuppressWarnings("static-method")
+       public Reaction loadReaction(Chain chain) throws LoaderException {
+
+               /* check if chain is enabled. */
+               if (!chain.enabled()) {
+                       throw new IllegalArgumentException("Chain is not enabled.");
+               }
+
+               /* create query. */
+               Query query = createObject(chain.query().name(), "net.pterodactylus.rhynodge.queries", extractParameters(chain.query().parameters()));
+
+               /* create filters. */
+               List<Filter> filters = new ArrayList<Filter>();
+               for (Part filterPart : chain.filters()) {
+                       filters.add(ReactionLoader.<Filter> createObject(filterPart.name(), "net.pterodactylus.rhynodge.filters", extractParameters(filterPart.parameters())));
+               }
+
+               /* create trigger. */
+               Trigger trigger = createObject(chain.trigger().name(), "net.pterodactylus.rhynodge.triggers", extractParameters(chain.trigger().parameters()));
+
+               /* create action. */
+               Action action = createObject(chain.action().name(), "net.pterodactylus.rhynodge.actions", extractParameters(chain.action().parameters()));
+
+               return new Reaction(chain.name(), query, filters, trigger, action).setUpdateInterval(TimeUnit.SECONDS.toMillis(chain.updateInterval()));
+       }
+
+       //
+       // STATIC METHODS
+       //
+
+       /**
+        * Extracts all parameter values from the given parameters.
+        *
+        * @param parameters
+        *            The parameters to extract the values from
+        * @return The extracted values
+        */
+       private static List<String> extractParameters(List<Parameter> parameters) {
+               List<String> parameterValues = new ArrayList<String>();
+
+               for (Parameter parameter : parameters) {
+                       parameterValues.add(parameter.value());
+               }
+
+               return parameterValues;
+       }
+
+       /**
+        * Creates a new object.
+        * <p>
+        * First, {@code className} is used to try to load a {@link Class} with that
+        * name. If that fails, {@code packageName} is prepended to the class name.
+        * If no class can be found, a {@link LoaderException} will be thrown.
+        * <p>
+        * If a class could be located using the described method, a constructor
+        * will be searched that has the same number of {@link String} parameters as
+        * the given parameters. The parameters from the given parameters are then
+        * used in a constructor call to create the new object.
+        *
+        * @param className
+        *            The name of the class
+        * @param packageName
+        *            The optional name of the package to prepend
+        * @param parameters
+        *            The parameters for the constructor call
+        * @return The created object
+        * @throws LoaderException
+        *             if the object can not be created
+        */
+       @SuppressWarnings("unchecked")
+       private static <T> T createObject(String className, String packageName, List<String> parameters) throws LoaderException {
+
+               /* try to load class without package name. */
+               Class<?> objectClass = null;
+               try {
+                       objectClass = Class.forName(className);
+               } catch (ClassNotFoundException cnfe1) {
+                       /* ignore, we’ll try again. */
+               }
+
+               if (objectClass == null) {
+                       try {
+                               objectClass = Class.forName(packageName + "." + className);
+                       } catch (ClassNotFoundException cnfe1) {
+                               /* okay, now we need to throw. */
+                               throw new LoaderException(String.format("Could find neither class “%s” nor class “%s.”", className, packageName + "." + className), cnfe1);
+                       }
+               }
+
+               /* locate an eligible constructor. */
+               Constructor<?> wantedConstructor = null;
+               for (Constructor<?> constructor : objectClass.getConstructors()) {
+                       Class<?>[] parameterTypes = constructor.getParameterTypes();
+                       if (parameterTypes.length != parameters.size()) {
+                               continue;
+                       }
+                       boolean compatibleTypes = true;
+                       for (Class<?> parameterType : parameterTypes) {
+                               if (parameterType != String.class) {
+                                       compatibleTypes = false;
+                                       break;
+                               }
+                       }
+                       if (!compatibleTypes) {
+                               continue;
+                       }
+                       wantedConstructor = constructor;
+               }
+
+               if (wantedConstructor == null) {
+                       throw new LoaderException("Could not find eligible constructor.");
+               }
+
+               try {
+                       return (T) wantedConstructor.newInstance(parameters.toArray());
+               } catch (IllegalArgumentException iae1) {
+                       throw new LoaderException("Could not invoke constructor.", iae1);
+               } catch (InstantiationException ie1) {
+                       throw new LoaderException("Could not invoke constructor.", ie1);
+               } catch (IllegalAccessException iae1) {
+                       throw new LoaderException("Could not invoke constructor.", iae1);
+               } catch (InvocationTargetException ite1) {
+                       throw new LoaderException("Could not invoke constructor.", ite1);
+               }
+
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/rhynodge/output/DefaultOutput.java b/src/main/java/net/pterodactylus/rhynodge/output/DefaultOutput.java
new file mode 100644 (file)
index 0000000..bf77aff
--- /dev/null
@@ -0,0 +1,85 @@
+/*
+ * Rhynodge - DefaultOutput.java - Copyright © 2013 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.rhynodge.output;
+
+import java.util.Map;
+
+import com.google.common.collect.Maps;
+
+/**
+ * {@link Output} implementation that stores texts for arbitrary MIME types.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class DefaultOutput implements Output {
+
+       /** The summary of the output. */
+       private final String summary;
+
+       /** The texts for the different MIME types. */
+       private final Map<String, String> mimeTypeTexts = Maps.newHashMap();
+
+       /**
+        * Creates a new default output.
+        *
+        * @param summary
+        *            The summary of the output
+        */
+       public DefaultOutput(String summary) {
+               this.summary = summary;
+       }
+
+       //
+       // ACTIONS
+       //
+
+       /**
+        * Adds the given text for the given MIME type.
+        *
+        * @param mimeType
+        *            The MIME type to add the text for
+        * @param text
+        *            The text to add
+        * @return This default output
+        */
+       public DefaultOutput addText(String mimeType, String text) {
+               mimeTypeTexts.put(mimeType, text);
+               return this;
+       }
+
+       //
+       // OUTPUT METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public String summary() {
+               return summary;
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public String text(String mimeType, int maxLength) {
+               return mimeTypeTexts.get(mimeType);
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/rhynodge/output/Output.java b/src/main/java/net/pterodactylus/rhynodge/output/Output.java
new file mode 100644 (file)
index 0000000..dc058bb
--- /dev/null
@@ -0,0 +1,56 @@
+/*
+ * Rhynodge - Output.java - Copyright © 2013 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.rhynodge.output;
+
+import net.pterodactylus.rhynodge.Trigger;
+
+/**
+ * Defines the output of a {@link Trigger}. As different output has to be
+ * generated for different media, the {@link #text(String, int)} method takes as
+ * an argument the MIME type of the desired output.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public interface Output {
+
+       /**
+        * Returns a short summary that can be included e. g. in the subject of an
+        * email.
+        *
+        * @return A short summary of the output
+        */
+       String summary();
+
+       /**
+        * Returns the text for the given MIME type and the given maximum length.
+        * Note that the maximum length does not need to be enforced at all costs;
+        * implementation are free to return texts longer than the given number of
+        * characters.
+        *
+        * @param mimeType
+        *            The MIME type of the text (“text/plain” and “text/html” should
+        *            be supported by all {@link Trigger}s)
+        * @param maxLength
+        *            The maximum length of the returned text (may be &lt; {@code 0}
+        *            to indicate no length restriction)
+        * @return The text for the given MIME type, or {@code null} if there is no
+        *         text defined for the given MIME type
+        */
+       String text(String mimeType, int maxLength);
+
+}
diff --git a/src/main/java/net/pterodactylus/rhynodge/package-info.java b/src/main/java/net/pterodactylus/rhynodge/package-info.java
new file mode 100644 (file)
index 0000000..2e7d9df
--- /dev/null
@@ -0,0 +1,25 @@
+/**
+ * Rhynodge main definitions.
+ * <p>
+ * A {@link net.pterodactylus.rhynodge.Reaction} consists of three different
+ * elements: a {@link net.pterodactylus.rhynodge.Query}, a
+ * {@link net.pterodactylus.rhynodge.Trigger}, and an
+ * {@link net.pterodactylus.rhynodge.Action}.
+ * <p>
+ * A {@code Query} retrieves the current state of a system; this can simply be
+ * the current state of a local file, or it can be the last tweet of a certain
+ * Twitter account, or it can be anything inbetween, or something completely
+ * different.
+ * <p>
+ * After a {@code Query} retrieved the current
+ * {@link net.pterodactylus.rhynodge.State} of a system, this state and the
+ * previously retrieved state are handed in to a {@code Trigger}. The trigger
+ * then decides whether the state of the system can be considered a change.
+ * <p>
+ * If a system has been found to trigger, an {@code Action} is executed. It
+ * performs arbitrary actions and can use both the current state and the
+ * previous state to define that action.
+ */
+
+package net.pterodactylus.rhynodge;
+
diff --git a/src/main/java/net/pterodactylus/rhynodge/queries/FileQuery.java b/src/main/java/net/pterodactylus/rhynodge/queries/FileQuery.java
new file mode 100644 (file)
index 0000000..68f561e
--- /dev/null
@@ -0,0 +1,67 @@
+/*
+ * Rhynodge - FileQuery.java - Copyright © 2013 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.rhynodge.queries;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import java.io.File;
+
+import net.pterodactylus.rhynodge.Query;
+import net.pterodactylus.rhynodge.State;
+import net.pterodactylus.rhynodge.states.FileState;
+
+/**
+ * Queries the filesystem about a file.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class FileQuery implements Query {
+
+       /** The name of the file to query. */
+       private final String filename;
+
+       /**
+        * Creates a new file query.
+        *
+        * @param filename
+        *            The name of the file to query
+        */
+       public FileQuery(String filename) {
+               this.filename = checkNotNull(filename, "filename must not be null");
+       }
+
+       //
+       // QUERY METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public State state() {
+               File file = new File(filename);
+               if (!file.exists()) {
+                       return new FileState(false, false, -1, -1);
+               }
+               if (!file.canRead()) {
+                       return new FileState(true, false, -1, -1);
+               }
+               return new FileState(true, true, file.length(), file.lastModified());
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/rhynodge/queries/HttpQuery.java b/src/main/java/net/pterodactylus/rhynodge/queries/HttpQuery.java
new file mode 100644 (file)
index 0000000..05f0830
--- /dev/null
@@ -0,0 +1,92 @@
+/*
+ * Rhynodge - HttpQuery.java - Copyright © 2013 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.rhynodge.queries;
+
+import java.io.IOException;
+import java.io.InputStreamReader;
+
+import net.pterodactylus.rhynodge.Query;
+import net.pterodactylus.rhynodge.State;
+import net.pterodactylus.rhynodge.states.FailedState;
+import net.pterodactylus.rhynodge.states.HttpState;
+
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpResponse;
+import org.apache.http.HttpStatus;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.protocol.ResponseContentEncoding;
+import org.apache.http.impl.client.DefaultHttpClient;
+import org.apache.http.util.EntityUtils;
+
+import com.google.common.io.Closeables;
+
+/**
+ * {@link Query} that performs an HTTP GET request to a fixed uri.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class HttpQuery implements Query {
+
+       /** The uri to request. */
+       private final String uri;
+
+       /**
+        * Creates a new HTTP query.
+        *
+        * @param uri
+        *            The uri to request
+        */
+       public HttpQuery(String uri) {
+               this.uri = uri;
+       }
+
+       //
+       // QUERY METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       @SuppressWarnings("deprecation")
+       public State state() {
+               DefaultHttpClient httpClient = new DefaultHttpClient();
+               httpClient.addResponseInterceptor(new ResponseContentEncoding());
+               HttpGet get = new HttpGet(uri);
+
+               InputStreamReader inputStreamReader = null;
+               try {
+                       /* make request. */
+                       get.addHeader("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/536.11 (KHTML, like Gecko) Ubuntu/12.04 Chromium/20.0.1132.47 Chrome/20.0.1132.47 Safari/536.11");
+                       HttpResponse response = httpClient.execute(get);
+                       if (response.getStatusLine().getStatusCode() != HttpStatus.SC_OK) {
+                               return new FailedState();
+                       }
+                       HttpEntity entity = response.getEntity();
+
+                       /* yay, done! */
+                       return new HttpState(uri, response.getStatusLine().getStatusCode(), entity.getContentType().getValue(), EntityUtils.toByteArray(entity));
+
+               } catch (IOException ioe1) {
+                       return new FailedState(ioe1);
+               } finally {
+                       Closeables.closeQuietly(inputStreamReader);
+               }
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/rhynodge/states/AbstractState.java b/src/main/java/net/pterodactylus/rhynodge/states/AbstractState.java
new file mode 100644 (file)
index 0000000..17cd518
--- /dev/null
@@ -0,0 +1,135 @@
+/*
+ * Rhynodge - AbstractState.java - Copyright © 2013 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.rhynodge.states;
+
+import net.pterodactylus.rhynodge.State;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
+
+/**
+ * Abstract implementation of a {@link State} that knows about the basic
+ * attributes of a {@link State}.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY, property = "@class")
+public abstract class AbstractState implements State {
+
+       /** The time of this state. */
+       @JsonProperty
+       private final long time;
+
+       /** Whether the state was successfully retrieved. */
+       private final boolean success;
+
+       /** The optional exception that occured while retrieving the state. */
+       private final Throwable exception;
+
+       /** The number of consecutive failures. */
+       @JsonProperty
+       private int failCount;
+
+       /**
+        * Creates a new successful state.
+        */
+       protected AbstractState() {
+               this(true);
+       }
+
+       /**
+        * Creates a new state.
+        *
+        * @param success
+        *            {@code true} if the state is successful, {@code false}
+        *            otherwise
+        */
+       protected AbstractState(boolean success) {
+               this(success, null);
+       }
+
+       /**
+        * Creates a new non-successful state with the given exception.
+        *
+        * @param exception
+        *            The exception that occured while retrieving the state
+        */
+       protected AbstractState(Throwable exception) {
+               this(false, exception);
+       }
+
+       /**
+        * Creates a new state.
+        *
+        * @param success
+        *            {@code true} if the state is successful, {@code false}
+        *            otherwise
+        * @param exception
+        *            The exception that occured while retrieving the state
+        */
+       protected AbstractState(boolean success, Throwable exception) {
+               this.time = System.currentTimeMillis();
+               this.success = success;
+               this.exception = exception;
+       }
+
+       //
+       // STATE METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public long time() {
+               return time;
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public boolean success() {
+               return success;
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public int failCount() {
+               return failCount;
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public void setFailCount(int failCount) {
+               this.failCount = failCount;
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public Throwable exception() {
+               return exception;
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/rhynodge/states/EpisodeState.java b/src/main/java/net/pterodactylus/rhynodge/states/EpisodeState.java
new file mode 100644 (file)
index 0000000..d7d90b2
--- /dev/null
@@ -0,0 +1,236 @@
+/*
+ * Rhynodge - EpisodeState.java - Copyright © 2013 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.rhynodge.states;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+
+import net.pterodactylus.rhynodge.State;
+import net.pterodactylus.rhynodge.filters.EpisodeFilter;
+import net.pterodactylus.rhynodge.states.EpisodeState.Episode;
+import net.pterodactylus.rhynodge.states.TorrentState.TorrentFile;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * {@link State} implementation that stores episodes of TV shows, parsed via
+ * {@link EpisodeFilter} from a previous {@link TorrentState}.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class EpisodeState extends AbstractState implements Iterable<Episode> {
+
+       /** The episodes found in the current request. */
+       @JsonProperty
+       private final List<Episode> episodes = new ArrayList<Episode>();
+
+       /**
+        * 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
+        */
+       public EpisodeState(Collection<Episode> episodes) {
+               this.episodes.addAll(episodes);
+       }
+
+       //
+       // ACCESSORS
+       //
+
+       /**
+        * Returns all episodes contained in this state.
+        *
+        * @return The episodes of this state
+        */
+       public Collection<Episode> episodes() {
+               return Collections.unmodifiableCollection(episodes);
+       }
+
+       //
+       // ITERABLE INTERFACE
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public Iterator<Episode> iterator() {
+               return episodes.iterator();
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public String toString() {
+               return String.format("%s[episodes=%s]", getClass().getSimpleName(), episodes);
+       }
+
+       /**
+        * Stores attributes for an episode.
+        *
+        * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+        */
+       public static class Episode implements Iterable<TorrentFile> {
+
+               /** The season of the episode. */
+               @JsonProperty
+               private final int season;
+
+               /** The number of the episode. */
+               @JsonProperty
+               private final int episode;
+
+               /** The torrent files for this episode. */
+               @JsonProperty
+               private final List<TorrentFile> torrentFiles = new ArrayList<TorrentFile>();
+
+               /**
+                * No-arg constructor for deserialization.
+                */
+               @SuppressWarnings("unused")
+               private Episode() {
+                       this(0, 0);
+               }
+
+               /**
+                * Creates a new episode.
+                *
+                * @param season
+                *            The season of the episode
+                * @param episode
+                *            The number of the episode
+                */
+               public Episode(int season, int episode) {
+                       this.season = season;
+                       this.episode = episode;
+               }
+
+               //
+               // ACCESSORS
+               //
+
+               /**
+                * Returns the season of this episode.
+                *
+                * @return The season of this episode
+                */
+               public int season() {
+                       return season;
+               }
+
+               /**
+                * Returns the number of this episode.
+                *
+                * @return The number of this episode
+                */
+               public int episode() {
+                       return episode;
+               }
+
+               /**
+                * Returns the torrent files of this episode.
+                *
+                * @return The torrent files of this episode
+                */
+               public Collection<TorrentFile> torrentFiles() {
+                       return torrentFiles;
+               }
+
+               /**
+                * Returns the identifier of this episode.
+                *
+                * @return The identifier of this episode
+                */
+               public String identifier() {
+                       return String.format("S%02dE%02d", season, episode);
+               }
+
+               //
+               // ACTIONS
+               //
+
+               /**
+                * Adds the given torrent file to this episode.
+                *
+                * @param torrentFile
+                *            The torrent file to add
+                */
+               public void addTorrentFile(TorrentFile torrentFile) {
+                       torrentFiles.add(torrentFile);
+               }
+
+               //
+               // ITERABLE METHODS
+               //
+
+               /**
+                * {@inheritDoc}
+                */
+               @Override
+               public Iterator<TorrentFile> iterator() {
+                       return torrentFiles.iterator();
+               }
+
+               //
+               // OBJECT METHODS
+               //
+
+               /**
+                * {@inheritDoc}
+                */
+               @Override
+               public int hashCode() {
+                       return season * 65536 + episode;
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               @Override
+               public boolean equals(Object obj) {
+                       if (!(obj instanceof Episode)) {
+                               return false;
+                       }
+                       Episode episode = (Episode) obj;
+                       return (season == episode.season) && (this.episode == episode.episode);
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               @Override
+               public String toString() {
+                       return String.format("%s[season=%d,episode=%d,torrentFiles=%s]", getClass().getSimpleName(), season, episode, torrentFiles);
+               }
+
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/rhynodge/states/FailedState.java b/src/main/java/net/pterodactylus/rhynodge/states/FailedState.java
new file mode 100644 (file)
index 0000000..89aa8f2
--- /dev/null
@@ -0,0 +1,81 @@
+/*
+ * Rhynodge - FailedState.java - Copyright © 2013 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.rhynodge.states;
+
+import net.pterodactylus.rhynodge.State;
+
+/**
+ * {@link State} implementation that signals failure.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class FailedState extends AbstractState {
+
+       /** A failed state instance without an exception. */
+       public static final State INSTANCE = new FailedState();
+
+       /**
+        * Creates a new failed state.
+        */
+       public FailedState() {
+               super(false);
+       }
+
+       /**
+        * Creates a new failed state with the given exception
+        *
+        * @param exception
+        *            The exception of the state
+        */
+       public FailedState(Throwable exception) {
+               super(exception);
+       }
+
+       //
+       // STATIC METHODS
+       //
+
+       /**
+        * Returns a failed state for the given state. The failed state will be
+        * unsuccessful ({@link #success()} returns false) and it will contain the
+        * same {@link #exception()} as the given state.
+        *
+        * @param state
+        *            The state to copy the exception from
+        * @return A failed state
+        */
+       public static FailedState from(State state) {
+               if (state instanceof FailedState) {
+                       return (FailedState) state;
+               }
+               return new FailedState(state.exception());
+       }
+
+       //
+       // OBJECT METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public String toString() {
+               return String.format("%s[exception=%s]", getClass().getSimpleName(), exception());
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/rhynodge/states/FileState.java b/src/main/java/net/pterodactylus/rhynodge/states/FileState.java
new file mode 100644 (file)
index 0000000..3c2e999
--- /dev/null
@@ -0,0 +1,129 @@
+/*
+ * Rhynodge - FileState.java - Copyright © 2013 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.rhynodge.states;
+
+import net.pterodactylus.rhynodge.State;
+
+/**
+ * A {@link State} that contains information about a file.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class FileState extends AbstractState {
+
+       /** Whether the file exists. */
+       private final boolean exists;
+
+       /** Whether the file is readable. */
+       private final boolean readable;
+
+       /** The size of the file. */
+       private final long size;
+
+       /** The modification time of the file. */
+       private final long modificationTime;
+
+       /**
+        * Creates a new file state that signals that an exceptio occured during
+        * retrieval.
+        *
+        * @param exception
+        *            The exception that occured
+        */
+       public FileState(Throwable exception) {
+               super(exception);
+               exists = false;
+               readable = false;
+               size = -1;
+               modificationTime = -1;
+       }
+
+       /**
+        * Creates a new file state.
+        *
+        * @param exists
+        *            {@code true} if the file exists, {@code false} otherwise
+        * @param readable
+        *            {@code true} if the file is readable, {@code false} otherwise
+        * @param size
+        *            The size of the file (in bytes)
+        * @param modificationTime
+        *            The modification time of the file (in milliseconds since Jan
+        *            1, 1970 UTC)
+        */
+       public FileState(boolean exists, boolean readable, long size, long modificationTime) {
+               this.exists = exists;
+               this.readable = readable;
+               this.size = size;
+               this.modificationTime = modificationTime;
+       }
+
+       //
+       // ACCESSORS
+       //
+
+       /**
+        * Returns whether the file exists.
+        *
+        * @return {@code true} if the file exists, {@code false} otherwise
+        */
+       public boolean exists() {
+               return exists;
+       }
+
+       /**
+        * Returns whether the file is readable.
+        *
+        * @return {@code true} if the file is readable, {@code false} otherwise
+        */
+       public boolean readable() {
+               return readable;
+       }
+
+       /**
+        * Returns the size of the file.
+        *
+        * @return The size of the file (in bytes)
+        */
+       public long size() {
+               return size;
+       }
+
+       /**
+        * Returns the modification time of the file.
+        *
+        * @return The modification time of the file (in milliseconds since Jan 1,
+        *         1970 UTC)
+        */
+       public long modificationTime() {
+               return modificationTime;
+       }
+
+       //
+       // OBJECT METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public String toString() {
+               return String.format("%s[exists=%s,readable=%s,size=%s,modificationTime=%d(%5$tc)", getClass().getSimpleName(), exists(), readable(), size(), modificationTime());
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/rhynodge/states/HtmlState.java b/src/main/java/net/pterodactylus/rhynodge/states/HtmlState.java
new file mode 100644 (file)
index 0000000..64ac8a5
--- /dev/null
@@ -0,0 +1,84 @@
+/*
+ * Rhynodge - HtmlState.java - Copyright © 2013 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.rhynodge.states;
+
+import net.pterodactylus.rhynodge.State;
+
+import org.jsoup.nodes.Document;
+
+/**
+ * {@link State} implementation that contains a parsed HTML {@link Document}.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class HtmlState extends AbstractState {
+
+       /** The URI of the parsed document. */
+       private final String uri;
+
+       /** The parsed document. */
+       private final Document document;
+
+       /**
+        * Creates a new HTML state.
+        *
+        * @param uri
+        *            The URI of the parsed document
+        * @param document
+        *            The parsed documnet
+        */
+       public HtmlState(String uri, Document document) {
+               this.uri = uri;
+               this.document = document;
+       }
+
+       //
+       // ACCESSORS
+       //
+
+       /**
+        * Returns the URI of the parsed document.
+        *
+        * @return The URI of the parsed document
+        */
+       public String uri() {
+               return uri;
+       }
+
+       /**
+        * Returns the parsed document.
+        *
+        * @return The parsed document
+        */
+       public Document document() {
+               return document;
+       }
+
+       //
+       // OBJECT METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public String toString() {
+               return String.format("%s[document=(%s chars)]", getClass().getSimpleName(), document().toString().length());
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/rhynodge/states/HttpState.java b/src/main/java/net/pterodactylus/rhynodge/states/HttpState.java
new file mode 100644 (file)
index 0000000..95ea051
--- /dev/null
@@ -0,0 +1,154 @@
+/*
+ * Rhynodge - HttpState.java - Copyright © 2013 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.rhynodge.states;
+
+import java.io.UnsupportedEncodingException;
+
+import net.pterodactylus.rhynodge.State;
+import net.pterodactylus.rhynodge.queries.HttpQuery;
+
+import org.apache.http.HeaderElement;
+import org.apache.http.NameValuePair;
+import org.apache.http.message.BasicHeaderValueParser;
+
+/**
+ * {@link State} that contains the results of an {@link HttpQuery}.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class HttpState extends AbstractState {
+
+       /** The URI that was requested. */
+       private final String uri;
+
+       /** The protocol code. */
+       private final int protocolCode;
+
+       /** The content type. */
+       private final String contentType;
+
+       /** The result. */
+       private final byte[] rawResult;
+
+       /**
+        * Creates a new HTTP state.
+        *
+        * @param uri
+        *            The URI that was requested
+        * @param protocolCode
+        *            The code of the reply
+        * @param contentType
+        *            The content type of the reply
+        * @param rawResult
+        *            The raw result
+        */
+       public HttpState(String uri, int protocolCode, String contentType, byte[] rawResult) {
+               this.uri = uri;
+               this.protocolCode = protocolCode;
+               this.contentType = contentType;
+               this.rawResult = rawResult;
+       }
+
+       //
+       // ACCESSORS
+       //
+
+       /**
+        * Returns the URI that was requested.
+        *
+        * @return The URI that was request
+        */
+       public String uri() {
+               return uri;
+       }
+
+       /**
+        * Returns the protocol code of the reply.
+        *
+        * @return The protocol code of the reply
+        */
+       public int protocolCode() {
+               return protocolCode;
+       }
+
+       /**
+        * Returns the content type of the reply.
+        *
+        * @return The content type of the reply
+        */
+       public String contentType() {
+               return contentType;
+       }
+
+       /**
+        * Returns the raw result of the reply.
+        *
+        * @return The raw result of the reply
+        */
+       public byte[] rawResult() {
+               return rawResult;
+       }
+
+       /**
+        * Returns the decoded content of the reply. This method uses the charset
+        * information from the {@link #contentType()}, if present, or UTF-8 if no
+        * content type is present.
+        *
+        * @return The decoded content
+        */
+       public String content() {
+               try {
+                       return new String(rawResult(), extractCharset(contentType()));
+               } catch (UnsupportedEncodingException uee1) {
+                       throw new RuntimeException(String.format("Could not decode content as %s.", extractCharset(contentType())), uee1);
+               }
+       }
+
+       //
+       // STATIC METHODS
+       //
+
+       /**
+        * Extracts charset information from the given content type.
+        *
+        * @param contentType
+        *            The content type response header
+        * @return The extracted charset, or UTF-8 if no charset could be extracted
+        */
+       private static String extractCharset(String contentType) {
+               if (contentType == null) {
+                       return "ISO-8859-1";
+               }
+               HeaderElement headerElement = BasicHeaderValueParser.parseHeaderElement(contentType, new BasicHeaderValueParser());
+               NameValuePair charset = headerElement.getParameterByName("charset");
+               return (charset != null) ? charset.getValue() : "ISO-8859-1";
+       }
+
+       //
+       // OBJECT METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public String toString() {
+               return String.format("%s[uri=%s,protocolCode=%d,contentType=%s,rawResult=(%s bytes)]", getClass().getSimpleName(), uri(), protocolCode(), contentType(), rawResult().length);
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/rhynodge/states/StateManager.java b/src/main/java/net/pterodactylus/rhynodge/states/StateManager.java
new file mode 100644 (file)
index 0000000..19eccba
--- /dev/null
@@ -0,0 +1,154 @@
+/*
+ * Rhynodge - StateManager.java - Copyright © 2013 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.rhynodge.states;
+
+import java.io.File;
+import java.io.IOException;
+
+import net.pterodactylus.rhynodge.State;
+
+import org.apache.log4j.Logger;
+
+import com.fasterxml.jackson.core.JsonGenerationException;
+import com.fasterxml.jackson.core.JsonParseException;
+import com.fasterxml.jackson.databind.JsonMappingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+/**
+ * Loads and saves {@link State}s.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class StateManager {
+
+       /** The logger. */
+       private static final Logger logger = Logger.getLogger(StateManager.class);
+
+       /** Jackson object mapper. */
+       private final ObjectMapper objectMapper = new ObjectMapper();
+
+       /** The directory in which to store states. */
+       private final String directory;
+
+       /**
+        * Creates a new state manager. The given directory is assumed to exist.
+        *
+        * @param directory
+        *            The directory to store states in
+        */
+       public StateManager(String directory) {
+               this.directory = directory;
+       }
+
+       //
+       // ACTIONS
+       //
+
+       /**
+        * Loads the last state with the given name.
+        *
+        * @param reactionName
+        *            The name of the reaction
+        * @return The loaded state, or {@code null} if the state could not be
+        *         loaded
+        */
+       public State loadLastState(String reactionName) {
+               return loadLastState(reactionName, false);
+       }
+
+       /**
+        * Loads the last state with the given name.
+        *
+        * @param reactionName
+        *            The name of the reaction
+        * @return The loaded state, or {@code null} if the state could not be
+        *         loaded
+        */
+       public State loadLastSuccessfulState(String reactionName) {
+               return loadLastState(reactionName, true);
+       }
+
+       /**
+        * Saves the given state under the given name.
+        *
+        * @param reactionName
+        *            The name of the reaction
+        * @param state
+        *            The state to save
+        */
+       public void saveState(String reactionName, State state) {
+               try {
+                       File stateFile = stateFile(reactionName, "last");
+                       objectMapper.writeValue(stateFile, state);
+                       if (state.success()) {
+                               stateFile = stateFile(reactionName, "success");
+                               objectMapper.writeValue(stateFile, state);
+                       }
+               } catch (JsonGenerationException jge1) {
+                       logger.warn(String.format("State for Reaction “%s” could not be generated.", reactionName), jge1);
+               } catch (JsonMappingException jme1) {
+                       logger.warn(String.format("State for Reaction “%s” could not be generated.", reactionName), jme1);
+               } catch (IOException ioe1) {
+                       logger.warn(String.format("State for Reaction “%s” could not be written.", reactionName));
+               }
+       }
+
+       //
+       // PRIVATE METHODS
+       //
+
+       /**
+        * Returns the file for the state with the given name.
+        *
+        * @param reactionName
+        *            The name of the reaction
+        * @param suffix
+        *            An additional suffix (may be {@code null}
+        * @return The file for the state
+        */
+       private File stateFile(String reactionName, String suffix) {
+               return new File(directory, reactionName + ((suffix != null) ? "." + suffix : "") + ".json");
+       }
+
+       /**
+        * Load the given state for the reaction with the given name.
+        *
+        * @param reactionName
+        *            The name of the reaction
+        * @param successful
+        *            {@code true} to load the last successful state, {@code false}
+        *            to load the last state
+        * @return The loaded state, or {@code null} if the state could not be
+        *         loaded
+        */
+       private State loadLastState(String reactionName, boolean successful) {
+               File stateFile = stateFile(reactionName, successful ? "success" : "last");
+               try {
+                       State state = objectMapper.readValue(stateFile, AbstractState.class);
+                       return state;
+               } catch (JsonParseException jpe1) {
+                       logger.warn(String.format("State for Reaction “%s” could not be parsed.", reactionName), jpe1);
+               } catch (JsonMappingException jme1) {
+                       logger.warn(String.format("State for Reaction “%s” could not be parsed.", reactionName), jme1);
+               } catch (IOException ioe1) {
+                       logger.info(String.format("State for Reaction “%s” could not be found.", reactionName));
+               }
+               return null;
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/rhynodge/states/TorrentState.java b/src/main/java/net/pterodactylus/rhynodge/states/TorrentState.java
new file mode 100644 (file)
index 0000000..ce5b06f
--- /dev/null
@@ -0,0 +1,305 @@
+/*
+ * Rhynodge - TorrentState.java - Copyright © 2013 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.rhynodge.states;
+
+import java.nio.charset.Charset;
+import java.util.Iterator;
+import java.util.List;
+
+import net.pterodactylus.rhynodge.State;
+import net.pterodactylus.rhynodge.states.TorrentState.TorrentFile;
+
+import org.apache.http.NameValuePair;
+import org.apache.http.client.utils.URLEncodedUtils;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.common.collect.Lists;
+
+/**
+ * {@link State} that contains information about an arbitrary number of torrent
+ * files.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class TorrentState extends AbstractState implements Iterable<TorrentFile> {
+
+       /** The torrent files. */
+       @JsonProperty
+       private List<TorrentFile> files = Lists.newArrayList();
+
+       //
+       // ACCESSORS
+       //
+
+       /**
+        * Adds a torrent file to this state.
+        *
+        * @param torrentFile
+        *            The torrent file to add
+        * @return This state
+        */
+       public TorrentState addTorrentFile(TorrentFile torrentFile) {
+               files.add(torrentFile);
+               return this;
+       }
+
+       //
+       // ITERABLE METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public Iterator<TorrentFile> iterator() {
+               return files.iterator();
+       }
+
+       //
+       // OBJECT METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public String toString() {
+               return String.format("%s[files=%s]", getClass().getSimpleName(), files);
+       }
+
+       /**
+        * Container for torrent file data.
+        *
+        * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+        */
+       public static class TorrentFile {
+
+               /** The name of the file. */
+               @JsonProperty
+               private final String name;
+
+               /** The size of the file. */
+               @JsonProperty
+               private final String size;
+
+               /** The magnet URI of the file. */
+               @JsonProperty
+               private final String magnetUri;
+
+               /** The download URI of the file. */
+               @JsonProperty
+               private final String downloadUri;
+
+               /** The number of files in this torrent. */
+               @JsonProperty
+               private final int fileCount;
+
+               /** The number of seeds connected to this torrent. */
+               @JsonProperty
+               private final int seedCount;
+
+               /** The number of leechers connected to this torrent. */
+               @JsonProperty
+               private final int leechCount;
+
+               /**
+                * No-arg constructor for deserialization.
+                */
+               @SuppressWarnings("unused")
+               private TorrentFile() {
+                       this(null, null, null, null, 0, 0, 0);
+               }
+
+               /**
+                * Creates a new torrent file.
+                *
+                * @param name
+                *            The name of the file
+                * @param size
+                *            The size of the file
+                * @param magnetUri
+                *            The magnet URI of the file
+                * @param downloadUri
+                *            The download URI of the file
+                * @param fileCount
+                *            The number of files
+                * @param seedCount
+                *            The number of connected seeds
+                * @param leechCount
+                *            The number of connected leechers
+                */
+               public TorrentFile(String name, String size, String magnetUri, String downloadUri, int fileCount, int seedCount, int leechCount) {
+                       this.name = name;
+                       this.size = size;
+                       this.magnetUri = magnetUri;
+                       this.downloadUri = downloadUri;
+                       this.fileCount = fileCount;
+                       this.seedCount = seedCount;
+                       this.leechCount = leechCount;
+               }
+
+               //
+               // ACCESSORS
+               //
+
+               /**
+                * Returns the name of the file.
+                *
+                * @return The name of the file
+                */
+               public String name() {
+                       return name;
+               }
+
+               /**
+                * Returns the size of the file. The returned size may included
+                * non-numeric information, such as units (e. g. “860.46 MB”).
+                *
+                * @return The size of the file
+                */
+               public String size() {
+                       return size;
+               }
+
+               /**
+                * Returns the magnet URI of the file.
+                *
+                * @return The magnet URI of the file
+                */
+               public String magnetUri() {
+                       return magnetUri;
+               }
+
+               /**
+                * Returns the download URI of the file.
+                *
+                * @return The download URI of the file
+                */
+               public String downloadUri() {
+                       return downloadUri;
+               }
+
+               /**
+                * Returns the number of files in this torrent.
+                *
+                * @return The number of files in this torrent
+                */
+               public int fileCount() {
+                       return fileCount;
+               }
+
+               /**
+                * Returns the number of seeds connected to this torrent.
+                *
+                * @return The number of connected seeds
+                */
+               public int seedCount() {
+                       return seedCount;
+               }
+
+               /**
+                * Returns the number of leechers connected to this torrent.
+                *
+                * @return The number of connected leechers
+                */
+               public int leechCount() {
+                       return leechCount;
+               }
+
+               //
+               // PRIVATE METHODS
+               //
+
+               /**
+                * Generates an ID for this file. If a {@link #magnetUri} is set, an ID
+                * is {@link #extractId(String) extracted} from it. Otherwise the magnet
+                * URI is used. If the {@link #magnetUri} is not set, the
+                * {@link #downloadUri} is used. If that is not set either, the name of
+                * the file is returned.
+                *
+                * @return The generated ID
+                */
+               private String generateId() {
+                       if (magnetUri != null) {
+                               String id = extractId(magnetUri);
+                               if (id != null) {
+                                       return id;
+                               }
+                               return magnetUri;
+                       }
+                       return (downloadUri != null) ? downloadUri : name;
+               }
+
+               //
+               // STATIC METHODS
+               //
+
+               /**
+                * Tries to extract the “exact target” of a magnet URI.
+                *
+                * @param magnetUri
+                *            The magnet URI to extract the “xt” from
+                * @return The extracted ID, or {@code null} if no ID could be found
+                */
+               private static String extractId(String magnetUri) {
+                       List<NameValuePair> parameters = URLEncodedUtils.parse(magnetUri.substring("magnet:?".length()), Charset.forName("UTF-8"));
+                       for (NameValuePair parameter : parameters) {
+                               if (parameter.getName().equals("xt")) {
+                                       return parameter.getValue();
+                               }
+                       }
+                       return null;
+               }
+
+               //
+               // OBJECT METHODS
+               //
+
+               /**
+                * {@inheritDoc}
+                */
+               @Override
+               public int hashCode() {
+                       return (generateId() != null) ? generateId().hashCode() : 0;
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               @Override
+               public boolean equals(Object object) {
+                       if (!(object instanceof TorrentFile)) {
+                               return false;
+                       }
+                       if (generateId() != null) {
+                               return generateId().equals(((TorrentFile) object).generateId());
+                       }
+                       return false;
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               @Override
+               public String toString() {
+                       return String.format("%s(%s,%s,%s)", name(), size(), magnetUri(), downloadUri());
+               }
+
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/rhynodge/triggers/AlwaysTrigger.java b/src/main/java/net/pterodactylus/rhynodge/triggers/AlwaysTrigger.java
new file mode 100644 (file)
index 0000000..3716894
--- /dev/null
@@ -0,0 +1,49 @@
+/*
+ * Rhynodge - AlwaysTrigger.java - Copyright © 2013 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.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;
+
+/**
+ * {@link Trigger} implementation that always triggers.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class AlwaysTrigger implements Trigger {
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public boolean triggers(State currentState, State previousState) {
+               return true;
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public Output output(Reaction reaction) {
+               return new DefaultOutput("true").addText("text/plain", "true").addText("text/html", "<div>true</div>");
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/rhynodge/triggers/FileExistenceTrigger.java b/src/main/java/net/pterodactylus/rhynodge/triggers/FileExistenceTrigger.java
new file mode 100644 (file)
index 0000000..49d91b2
--- /dev/null
@@ -0,0 +1,58 @@
+/*
+ * Rhynodge - FileExistenceTrigger.java - Copyright © 2013 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.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.FileState;
+
+import com.google.common.base.Preconditions;
+
+/**
+ * A trigger that detects changes in the existence of a file.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class FileExistenceTrigger implements Trigger {
+
+       //
+       // TRIGGER METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public boolean triggers(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();
+       }
+
+       /**
+        * {@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>");
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/rhynodge/triggers/FileStateModifiedTrigger.java b/src/main/java/net/pterodactylus/rhynodge/triggers/FileStateModifiedTrigger.java
new file mode 100644 (file)
index 0000000..ed24d9e
--- /dev/null
@@ -0,0 +1,57 @@
+/*
+ * Rhynodge - FileStateModifiedTrigger.java - Copyright © 2013 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.rhynodge.triggers;
+
+import static com.google.common.base.Preconditions.checkState;
+
+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.FileState;
+
+/**
+ * {@link Trigger} that checks for modifications of a file using the existence,
+ * size, and modification time of the {@link FileState}.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class FileStateModifiedTrigger implements Trigger {
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public boolean triggers(State currentState, State previousState) {
+               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());
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public Output output(Reaction reaction) {
+               return new DefaultOutput("File modified").addText("text/plain", "File modified").addText("text/html", "<div>File modified</div>");
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/rhynodge/triggers/NewEpisodeTrigger.java b/src/main/java/net/pterodactylus/rhynodge/triggers/NewEpisodeTrigger.java
new file mode 100644 (file)
index 0000000..f23869d
--- /dev/null
@@ -0,0 +1,233 @@
+/*
+ * Rhynodge - NewEpisodeTrigger.java - Copyright © 2013 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.rhynodge.triggers;
+
+import static com.google.common.base.Preconditions.checkState;
+
+import java.util.Collection;
+
+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.Predicate;
+import com.google.common.collect.Collections2;
+
+/**
+ * {@link Trigger} implementation that compares two {@link EpisodeState}s for
+ * new and changed {@link Episode}s.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class NewEpisodeTrigger implements Trigger {
+
+       /** All new episodes. */
+       private Collection<Episode> newEpisodes;
+
+       /** All changed episodes. */
+       private Collection<Episode> changedEpisodes;
+
+       //
+       // TRIGGER METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public boolean triggers(State currentState, State previousState) {
+               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<Episode>() {
+
+                       @Override
+                       public boolean apply(Episode episode) {
+                               return !previousEpisodeState.episodes().contains(episode);
+                       }
+               });
+
+               changedEpisodes = Collections2.filter(currentEpisodeState.episodes(), new Predicate<Episode>() {
+
+                       @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<TorrentFile> newTorrentFiles = Collections2.filter(episode.torrentFiles(), new Predicate<TorrentFile>() {
+
+                                       @Override
+                                       public boolean apply(TorrentFile torrentFile) {
+                                               return !previousEpisode.torrentFiles().contains(torrentFile);
+                                       }
+                               });
+
+                               return !newTorrentFiles.isEmpty();
+                       }
+
+                       private Episode findPreviousEpisode(Episode episode) {
+                               for (Episode previousStateEpisode : previousEpisodeState) {
+                                       if (previousStateEpisode.equals(episode)) {
+                                               return previousStateEpisode;
+                                       }
+                               }
+                               return null;
+                       }
+
+               });
+
+               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, newEpisodes, changedEpisodes));
+               output.addText("text/html", generateHtmlText(reaction, newEpisodes, changedEpisodes));
+               return output;
+       }
+
+       //
+       // STATIC METHODS
+       //
+
+       /**
+        * Generates the plain text trigger output.
+        *
+        * @param reaction
+        *            The reaction that was triggered
+        * @param newEpisodes
+        *            The new episodes
+        * @param changedEpisodes
+        *            The changed episodes
+        * @return The plain text output
+        */
+       private static String generatePlainText(Reaction reaction, Collection<Episode> newEpisodes, Collection<Episode> changedEpisodes) {
+               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");
+                                       stringBuilder.append("    Magnet: ").append(torrentFile.magnetUri()).append("\n");
+                                       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");
+                                       stringBuilder.append("    Magnet: ").append(torrentFile.magnetUri()).append("\n");
+                                       stringBuilder.append("    Download: ").append(torrentFile.downloadUri()).append("\n");
+                               }
+                       }
+               }
+               return stringBuilder.toString();
+       }
+
+       /**
+        * Generates the HTML trigger output.
+        *
+        * @param reaction
+        *            The reaction that was triggered
+        * @param newEpisodes
+        *            The new episodes
+        * @param changedEpisodes
+        *            The changed episodes
+        * @return The HTML output
+        */
+       private static String generateHtmlText(Reaction reaction, Collection<Episode> newEpisodes, Collection<Episode> changedEpisodes) {
+               StringBuilder htmlBuilder = new StringBuilder();
+               htmlBuilder.append("<html><body>\n");
+               htmlBuilder.append("<h1>").append(StringEscapeUtils.escapeHtml4(reaction.name())).append("</h1>\n");
+               if (!newEpisodes.isEmpty()) {
+                       htmlBuilder.append("<h2>New Episodes</h2>\n");
+                       htmlBuilder.append("<ul>\n");
+                       for (Episode episode : newEpisodes) {
+                               htmlBuilder.append("<li>Season ").append(episode.season()).append(", Episode ").append(episode.episode()).append("</li>\n");
+                               htmlBuilder.append("<ul>\n");
+                               for (TorrentFile torrentFile : episode) {
+                                       htmlBuilder.append("<li>").append(StringEscapeUtils.escapeHtml4(torrentFile.name())).append("</li>\n");
+                                       htmlBuilder.append("<div>");
+                                       htmlBuilder.append("<strong>").append(StringEscapeUtils.escapeHtml4(torrentFile.size())).append("</strong>, ");
+                                       htmlBuilder.append("<strong>").append(torrentFile.fileCount()).append("</strong> file(s), ");
+                                       htmlBuilder.append("<strong>").append(torrentFile.seedCount()).append("</strong> seed(s), ");
+                                       htmlBuilder.append("<strong>").append(torrentFile.leechCount()).append("</strong> leecher(s)</div>\n");
+                                       htmlBuilder.append("<div><a href=\"").append(StringEscapeUtils.escapeHtml4(torrentFile.magnetUri())).append("\">Magnet</a> ");
+                                       htmlBuilder.append("<a href=\"").append(StringEscapeUtils.escapeHtml4(torrentFile.downloadUri())).append("\">Download</a></div>\n");
+                               }
+                               htmlBuilder.append("</ul>\n");
+                       }
+                       htmlBuilder.append("</ul>\n");
+               }
+               if (!changedEpisodes.isEmpty()) {
+                       htmlBuilder.append("<h2>Changed Episodes</h2>\n");
+                       htmlBuilder.append("<ul>\n");
+                       for (Episode episode : changedEpisodes) {
+                               htmlBuilder.append("<li>Season ").append(episode.season()).append(", Episode ").append(episode.episode()).append("</li>\n");
+                               htmlBuilder.append("<ul>\n");
+                               for (TorrentFile torrentFile : episode) {
+                                       htmlBuilder.append("<li>").append(StringEscapeUtils.escapeHtml4(torrentFile.name())).append("</li>\n");
+                                       htmlBuilder.append("<div>");
+                                       htmlBuilder.append("<strong>").append(StringEscapeUtils.escapeHtml4(torrentFile.size())).append("</strong>, ");
+                                       htmlBuilder.append("<strong>").append(torrentFile.fileCount()).append("</strong> file(s), ");
+                                       htmlBuilder.append("<strong>").append(torrentFile.seedCount()).append("</strong> seed(s), ");
+                                       htmlBuilder.append("<strong>").append(torrentFile.leechCount()).append("</strong> leecher(s)</div>\n");
+                                       htmlBuilder.append("<div><a href=\"").append(StringEscapeUtils.escapeHtml4(torrentFile.magnetUri())).append("\">Magnet</a> ");
+                                       htmlBuilder.append("<a href=\"").append(StringEscapeUtils.escapeHtml4(torrentFile.downloadUri())).append("\">Download</a></div>\n");
+                               }
+                               htmlBuilder.append("</ul>\n");
+                       }
+                       htmlBuilder.append("</ul>\n");
+               }
+               htmlBuilder.append("</body></html>\n");
+               return htmlBuilder.toString();
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/rhynodge/triggers/NewTorrentTrigger.java b/src/main/java/net/pterodactylus/rhynodge/triggers/NewTorrentTrigger.java
new file mode 100644 (file)
index 0000000..a21a8d6
--- /dev/null
@@ -0,0 +1,130 @@
+/*
+ * Rhynodge - NewTorrentTrigger.java - Copyright © 2013 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.rhynodge.triggers;
+
+import static com.google.common.base.Preconditions.checkState;
+
+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.TorrentState;
+import net.pterodactylus.rhynodge.states.TorrentState.TorrentFile;
+
+import org.apache.commons.lang3.StringEscapeUtils;
+
+import com.google.common.collect.Lists;
+
+/**
+ * {@link Trigger} implementation that is triggered by {@link TorrentFile}s that
+ * appear in the current {@link TorrentState} but not in the previous one.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class NewTorrentTrigger implements Trigger {
+
+       /** The newly detected torrent files. */
+       private List<TorrentFile> torrentFiles = Lists.newArrayList();
+
+       //
+       // TRIGGER METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public boolean triggers(State currentState, State previousState) {
+               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();
+               for (TorrentFile torrentFile : currentTorrentState) {
+                       torrentFiles.add(torrentFile);
+               }
+               for (TorrentFile torrentFile : previousTorrentState) {
+                       torrentFiles.remove(torrentFile);
+               }
+               return !torrentFiles.isEmpty();
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @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));
+               return output;
+       }
+
+       //
+       // STATIC METHODS
+       //
+
+       /**
+        * Generates a plain text list of torrent files.
+        *
+        * @param torrentFiles
+        *            The torrent files to list
+        * @return The generated plain text
+        */
+       private static String getPlainTextList(List<TorrentFile> torrentFiles) {
+               StringBuilder plainText = new StringBuilder();
+               plainText.append("New Torrents:\n\n");
+               for (TorrentFile torrentFile : torrentFiles) {
+                       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");
+                       plainText.append('\t').append(torrentFile.magnetUri()).append('\n');
+                       plainText.append('\t').append(torrentFile.downloadUri()).append('\n');
+                       plainText.append('\n');
+               }
+               return plainText.toString();
+       }
+
+       /**
+        * Generates an HTML list of the given torrent files.
+        *
+        * @param torrentFiles
+        *            The torrent files to list
+        * @return The generated HTML
+        */
+       private static String getHtmlTextList(List<TorrentFile> torrentFiles) {
+               StringBuilder htmlText = new StringBuilder();
+               htmlText.append("<html><body>\n");
+               htmlText.append("<h1>New Torrents</h1>\n");
+               htmlText.append("<ul>\n");
+               for (TorrentFile torrentFile : torrentFiles) {
+                       htmlText.append("<li><strong>").append(StringEscapeUtils.escapeHtml4(torrentFile.name())).append("</strong></li>");
+                       htmlText.append("<div>Size: <strong>").append(StringEscapeUtils.escapeHtml4(torrentFile.size())).append("</strong> in <strong>").append(torrentFile.fileCount()).append("</strong> file(s)</div>");
+                       htmlText.append("<div><strong>").append(torrentFile.seedCount()).append("</strong> seed(s), <strong>").append(torrentFile.leechCount()).append("</strong> leecher(s)</div>");
+                       htmlText.append(String.format("<div><a href=\"%s\">Magnet URI</a></div>", StringEscapeUtils.escapeHtml4(torrentFile.magnetUri())));
+                       htmlText.append(String.format("<div><a href=\"%s\">Download URI</a></div>", StringEscapeUtils.escapeHtml4(torrentFile.downloadUri())));
+               }
+               htmlText.append("</ul>\n");
+               htmlText.append("</body></html>\n");
+               return htmlText.toString();
+       }
+
+}
index dbf0c7a..2962ea2 100644 (file)
                                },
                                {
                                        "name": "sender",
-                                       "value": "reactor@reactor.de"
+                                       "value": "rhynodge@rhynodge.net"
                                },
                                {
                                        "name": "recipient",
-                                       "value": "recipient@recipient.de"
+                                       "value": "recipient@recipient.net"
                                }
                        ]
                },
index bcdf5a8..77db92e 100644 (file)
@@ -4,5 +4,5 @@ log4j.appender.CA.layout=org.apache.log4j.PatternLayout
 log4j.appender.CA.layout.ConversionPattern=%d [%t] %p %c %m%n
 
 # some detailed logger settings
-log4j.logger.net.pterodactylus.reactor.loader.ChainWatcher=INFO
+log4j.logger.net.pterodactylus.rhynodge.loader.ChainWatcher=INFO
 log4j.logger.org.apache.http.wire=INFO