-# 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
## 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.
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>
<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>
+++ /dev/null
-/*
- * 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);
-
-}
+++ /dev/null
-/*
- * 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);
-
-}
+++ /dev/null
-/*
- * 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();
-
-}
+++ /dev/null
-/*
- * 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;
- }
-
-}
+++ /dev/null
-/*
- * 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();
-
-}
+++ /dev/null
-/*
- * 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);
-
-}
+++ /dev/null
-/*
- * 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. */
- }
- }
-
-}
+++ /dev/null
-/*
- * 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)));
- }
-
-}
+++ /dev/null
-/*
- * 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));
- }
-
- }
- }
-
-}
+++ /dev/null
-/*
- * 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();
-
- }
-
-}
+++ /dev/null
-/*
- * 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);
- }
-
-}
+++ /dev/null
-/*
- * 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);
- }
-
-}
+++ /dev/null
-/*
- * 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());
- }
-
-}
+++ /dev/null
-/*
- * 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;
- }
-
-}
+++ /dev/null
-/*
- * 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;
- }
-
-}
+++ /dev/null
-/*
- * 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);
- }
-
-}
+++ /dev/null
-/*
- * 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);
- }
-
- }
-
-}
+++ /dev/null
-/*
- * 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);
- }
-
-}
+++ /dev/null
-/*
- * 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 < {@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);
-
-}
+++ /dev/null
-/**
- * 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;
-
+++ /dev/null
-/*
- * 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());
- }
-
-}
+++ /dev/null
-/*
- * 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);
- }
- }
-
-}
+++ /dev/null
-/*
- * 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;
- }
-
-}
+++ /dev/null
-/*
- * 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);
- }
-
- }
-
-}
+++ /dev/null
-/*
- * 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());
- }
-
-}
+++ /dev/null
-/*
- * 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());
- }
-
-}
+++ /dev/null
-/*
- * 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());
- }
-
-}
+++ /dev/null
-/*
- * 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);
- }
-
-}
+++ /dev/null
-/*
- * 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;
- }
-
-}
+++ /dev/null
-/*
- * 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());
- }
-
- }
-
-}
+++ /dev/null
-/*
- * 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>");
- }
-
-}
+++ /dev/null
-/*
- * 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>");
- }
-
-}
+++ /dev/null
-/*
- * 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>");
- }
-
-}
+++ /dev/null
-/*
- * 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();
- }
-
-}
+++ /dev/null
-/*
- * 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();
- }
-
-}
--- /dev/null
+/*
+ * 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);
+
+}
--- /dev/null
+/*
+ * 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);
+
+}
--- /dev/null
+/*
+ * 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();
+
+}
--- /dev/null
+/*
+ * 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;
+ }
+
+}
--- /dev/null
+/*
+ * 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();
+
+}
--- /dev/null
+/*
+ * 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);
+
+}
--- /dev/null
+/*
+ * 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. */
+ }
+ }
+
+}
--- /dev/null
+/*
+ * 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)));
+ }
+
+}
--- /dev/null
+/*
+ * 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));
+ }
+
+ }
+ }
+
+}
--- /dev/null
+/*
+ * 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();
+
+ }
+
+}
--- /dev/null
+/*
+ * 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);
+ }
+
+}
--- /dev/null
+/*
+ * 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);
+ }
+
+}
--- /dev/null
+/*
+ * 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());
+ }
+
+}
--- /dev/null
+/*
+ * 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;
+ }
+
+}
--- /dev/null
+/*
+ * 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;
+ }
+
+}
--- /dev/null
+/*
+ * 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);
+ }
+
+}
--- /dev/null
+/*
+ * 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);
+ }
+
+ }
+
+}
--- /dev/null
+/*
+ * 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);
+ }
+
+}
--- /dev/null
+/*
+ * 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 < {@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);
+
+}
--- /dev/null
+/**
+ * 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;
+
--- /dev/null
+/*
+ * 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());
+ }
+
+}
--- /dev/null
+/*
+ * 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);
+ }
+ }
+
+}
--- /dev/null
+/*
+ * 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;
+ }
+
+}
--- /dev/null
+/*
+ * 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);
+ }
+
+ }
+
+}
--- /dev/null
+/*
+ * 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());
+ }
+
+}
--- /dev/null
+/*
+ * 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());
+ }
+
+}
--- /dev/null
+/*
+ * 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());
+ }
+
+}
--- /dev/null
+/*
+ * 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);
+ }
+
+}
--- /dev/null
+/*
+ * 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;
+ }
+
+}
--- /dev/null
+/*
+ * 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());
+ }
+
+ }
+
+}
--- /dev/null
+/*
+ * 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>");
+ }
+
+}
--- /dev/null
+/*
+ * 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>");
+ }
+
+}
--- /dev/null
+/*
+ * 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>");
+ }
+
+}
--- /dev/null
+/*
+ * 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();
+ }
+
+}
--- /dev/null
+/*
+ * 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();
+ }
+
+}
},
{
"name": "sender",
- "value": "reactor@reactor.de"
+ "value": "rhynodge@rhynodge.net"
},
{
"name": "recipient",
- "value": "recipient@recipient.de"
+ "value": "recipient@recipient.net"
}
]
},
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